Skip to main content

yb_core/piv/
mgmt.rs

1// SPDX-FileCopyrightText: 2026 Frederic Ruget <fred@atlant.is> (GitHub: @douzebis)
2//
3// SPDX-License-Identifier: MIT
4
5//! PIV management key algorithms and GET METADATA parsing (spec 0021).
6//!
7//! The management key's algorithm is read from the card (GET METADATA on
8//! slot 9B) instead of being inferred from the key length: a 24-byte key
9//! may be 3DES (firmware < 5.7 default) or AES-192 (firmware 5.7+ default).
10
11use anyhow::{bail, Result};
12use std::fmt;
13
14/// GET METADATA for the management key slot (9B).  Firmware 5.3+.
15pub const GET_METADATA_MGMT: [u8; 5] = [0x00, 0xF7, 0x00, 0x9B, 0x00];
16/// GET METADATA for the PIN (reference 0x80).  Firmware 5.3+.
17pub const GET_METADATA_PIN: [u8; 5] = [0x00, 0xF7, 0x00, 0x80, 0x00];
18/// GET METADATA for the PUK (reference 0x81).  Firmware 5.3+.
19pub const GET_METADATA_PUK: [u8; 5] = [0x00, 0xF7, 0x00, 0x81, 0x00];
20
21/// GET METADATA for a key slot.  Firmware 5.3+.
22pub(crate) fn get_metadata_apdu(slot: u8) -> [u8; 5] {
23    [0x00, 0xF7, 0x00, slot, 0x00]
24}
25
26// GET METADATA response tags.
27const TAG_ALGORITHM: u8 = 0x01;
28const TAG_POLICY: u8 = 0x02;
29const TAG_ORIGIN: u8 = 0x03;
30const TAG_IS_DEFAULT: u8 = 0x05;
31const TAG_RETRIES: u8 = 0x06;
32
33// Touch policy values (second byte of TAG_POLICY).
34const TOUCH_ALWAYS: u8 = 0x02;
35const TOUCH_CACHED: u8 = 0x03;
36
37/// Algorithm of the PIV management key (slot 9B).
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum MgmtAlgo {
40    Tdes,
41    Aes128,
42    Aes192,
43    Aes256,
44}
45
46impl MgmtAlgo {
47    /// Map a PIV algorithm identifier to a management key algorithm.
48    pub fn from_id(id: u8) -> Option<Self> {
49        match id {
50            0x03 => Some(Self::Tdes),
51            0x08 => Some(Self::Aes128),
52            0x0A => Some(Self::Aes192),
53            0x0C => Some(Self::Aes256),
54            _ => None,
55        }
56    }
57
58    /// PIV algorithm identifier (P1 of GENERAL AUTHENTICATE, first byte of
59    /// SET MANAGEMENT KEY data).
60    pub fn id(self) -> u8 {
61        match self {
62            Self::Tdes => 0x03,
63            Self::Aes128 => 0x08,
64            Self::Aes192 => 0x0A,
65            Self::Aes256 => 0x0C,
66        }
67    }
68
69    /// Key length in bytes.
70    pub fn key_len(self) -> usize {
71        match self {
72            Self::Aes128 => 16,
73            Self::Tdes | Self::Aes192 => 24,
74            Self::Aes256 => 32,
75        }
76    }
77
78    /// Cipher block size in bytes (challenge/witness length).
79    pub fn block_size(self) -> usize {
80        match self {
81            Self::Tdes => 8,
82            Self::Aes128 | Self::Aes192 | Self::Aes256 => 16,
83        }
84    }
85
86    /// Parse a name as used in fixture files: `TDES`, `AES128`, `AES192`,
87    /// `AES256` (case-insensitive; `3DES` and dashes are accepted).
88    pub fn from_name(name: &str) -> Option<Self> {
89        match name.to_ascii_uppercase().replace('-', "").as_str() {
90            "TDES" | "3DES" => Some(Self::Tdes),
91            "AES128" => Some(Self::Aes128),
92            "AES192" => Some(Self::Aes192),
93            "AES256" => Some(Self::Aes256),
94            _ => None,
95        }
96    }
97
98    /// Fail unless `key` has exactly this algorithm's key length.
99    pub fn check_key_len(self, key: &[u8]) -> Result<()> {
100        if key.len() != self.key_len() {
101            bail!(
102                "management key is {} bytes but the card uses {} ({} bytes)",
103                key.len(),
104                self,
105                self.key_len()
106            );
107        }
108        Ok(())
109    }
110}
111
112impl fmt::Display for MgmtAlgo {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        f.write_str(match self {
115            Self::Tdes => "3DES",
116            Self::Aes128 => "AES-128",
117            Self::Aes192 => "AES-192",
118            Self::Aes256 => "AES-256",
119        })
120    }
121}
122
123/// Touch policy of a key, from GET METADATA.
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum TouchPolicy {
126    Never,
127    Always,
128    Cached,
129}
130
131/// Management key metadata reported by GET METADATA (slot 9B).
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub struct MgmtMetadata {
134    pub algo: MgmtAlgo,
135    pub touch: TouchPolicy,
136    /// The key is the factory default.
137    pub is_default: bool,
138}
139
140impl MgmtMetadata {
141    /// Touch policy is "always" or "cached": authentication needs a touch.
142    pub fn touch_required(&self) -> bool {
143        self.touch != TouchPolicy::Never
144    }
145}
146
147/// PIN or PUK metadata reported by GET METADATA (reference 80/81).
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub struct CredentialMetadata {
150    /// The value is the factory default.
151    pub is_default: bool,
152    pub retries_total: u8,
153    pub retries_left: u8,
154}
155
156/// Where the key in a slot comes from, per GET METADATA.
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum KeyOrigin {
159    Generated,
160    Imported,
161}
162
163/// Key slot metadata reported by GET METADATA.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub struct SlotMetadata {
166    /// PIV algorithm identifier (e.g. `0x11` for EC P-256).
167    pub algorithm: u8,
168    pub origin: Option<KeyOrigin>,
169}
170
171/// Name of a PIV key algorithm identifier, for reports.
172pub fn key_algorithm_name(id: u8) -> String {
173    match id {
174        0x06 => "RSA 1024".to_owned(),
175        0x07 => "RSA 2048".to_owned(),
176        0x05 => "RSA 3072".to_owned(),
177        0x16 => "RSA 4096".to_owned(),
178        0x11 => "EC P-256".to_owned(),
179        0x14 => "EC P-384".to_owned(),
180        0xE0 => "Ed25519".to_owned(),
181        0xE1 => "X25519".to_owned(),
182        other => format!("algorithm 0x{other:02x}"),
183    }
184}
185
186/// PIV algorithm identifier of EC P-256.
187pub const ALGO_ECCP256: u8 = 0x11;
188
189fn is_default_flag(tlv: &std::collections::HashMap<u8, Vec<u8>>) -> bool {
190    tlv.get(&TAG_IS_DEFAULT).map(Vec::as_slice) == Some(&[0x01])
191}
192
193/// Parse the data of a GET METADATA (PIN or PUK) response.
194pub(crate) fn parse_credential_metadata(data: &[u8]) -> Result<CredentialMetadata> {
195    let tlv = crate::auxiliaries::parse_tlv_flat(data);
196    let (retries_total, retries_left) = match tlv.get(&TAG_RETRIES).map(Vec::as_slice) {
197        Some([total, left]) => (*total, *left),
198        _ => bail!("GET METADATA (PIN/PUK): missing retry counters"),
199    };
200    Ok(CredentialMetadata {
201        is_default: is_default_flag(&tlv),
202        retries_total,
203        retries_left,
204    })
205}
206
207/// Parse the data of a GET METADATA (key slot) response.
208pub(crate) fn parse_slot_metadata(data: &[u8]) -> Result<SlotMetadata> {
209    let tlv = crate::auxiliaries::parse_tlv_flat(data);
210    let algorithm = match tlv.get(&TAG_ALGORITHM).map(Vec::as_slice) {
211        Some([id]) => *id,
212        _ => bail!("GET METADATA (key slot): missing algorithm"),
213    };
214    let origin = match tlv.get(&TAG_ORIGIN).map(Vec::as_slice) {
215        Some([0x01]) => Some(KeyOrigin::Generated),
216        Some([0x02]) => Some(KeyOrigin::Imported),
217        _ => None,
218    };
219    Ok(SlotMetadata { algorithm, origin })
220}
221
222/// Parse the data of a GET METADATA (slot 9B) response.
223pub(crate) fn parse_mgmt_metadata(data: &[u8]) -> Result<MgmtMetadata> {
224    let tlv = crate::auxiliaries::parse_tlv_flat(data);
225    let id = match tlv.get(&TAG_ALGORITHM).map(Vec::as_slice) {
226        Some([id]) => *id,
227        _ => bail!("GET METADATA (management key): missing algorithm"),
228    };
229    let algo = MgmtAlgo::from_id(id)
230        .ok_or_else(|| anyhow::anyhow!("unsupported management key algorithm 0x{id:02x}"))?;
231    let touch = match tlv.get(&TAG_POLICY).map(Vec::as_slice) {
232        Some([_, TOUCH_ALWAYS]) => TouchPolicy::Always,
233        Some([_, TOUCH_CACHED]) => TouchPolicy::Cached,
234        _ => TouchPolicy::Never,
235    };
236    Ok(MgmtMetadata {
237        algo,
238        touch,
239        is_default: is_default_flag(&tlv),
240    })
241}
242
243/// Parse the data of a GET METADATA (PIN or PUK) response and return the
244/// number of retries remaining, or `None` if the response has no retry count.
245pub(crate) fn parse_retries_remaining(data: &[u8]) -> Option<u8> {
246    let tlv = crate::auxiliaries::parse_tlv_flat(data);
247    match tlv.get(&TAG_RETRIES).map(Vec::as_slice) {
248        Some([_total, remaining]) => Some(*remaining),
249        _ => None,
250    }
251}
252
253// ---------------------------------------------------------------------------
254// Tests
255// ---------------------------------------------------------------------------
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn ids_round_trip() {
263        for algo in [
264            MgmtAlgo::Tdes,
265            MgmtAlgo::Aes128,
266            MgmtAlgo::Aes192,
267            MgmtAlgo::Aes256,
268        ] {
269            assert_eq!(MgmtAlgo::from_id(algo.id()), Some(algo));
270        }
271        assert_eq!(MgmtAlgo::from_id(0x07), None);
272    }
273
274    #[test]
275    fn tdes_and_aes192_share_key_length() {
276        // The reason the algorithm cannot be inferred from the key length.
277        assert_eq!(MgmtAlgo::Tdes.key_len(), MgmtAlgo::Aes192.key_len());
278        assert_ne!(MgmtAlgo::Tdes.block_size(), MgmtAlgo::Aes192.block_size());
279    }
280
281    #[test]
282    fn from_name_accepts_fixture_spellings() {
283        assert_eq!(MgmtAlgo::from_name("TDES"), Some(MgmtAlgo::Tdes));
284        assert_eq!(MgmtAlgo::from_name("3des"), Some(MgmtAlgo::Tdes));
285        assert_eq!(MgmtAlgo::from_name("AES-192"), Some(MgmtAlgo::Aes192));
286        assert_eq!(MgmtAlgo::from_name("aes256"), Some(MgmtAlgo::Aes256));
287        assert_eq!(MgmtAlgo::from_name("RSA2048"), None);
288    }
289
290    #[test]
291    fn check_key_len_message() {
292        let err = MgmtAlgo::Aes192.check_key_len(&[0u8; 16]).unwrap_err();
293        assert_eq!(
294            err.to_string(),
295            "management key is 16 bytes but the card uses AES-192 (24 bytes)"
296        );
297        assert!(MgmtAlgo::Aes192.check_key_len(&[0u8; 24]).is_ok());
298    }
299
300    #[test]
301    fn parse_metadata_aes192_default() {
302        // As returned by a firmware 5.7 YubiKey with its factory key:
303        // algorithm AES-192, policy (PIN n/a, touch never), default = yes.
304        let data = [0x01, 0x01, 0x0A, 0x02, 0x02, 0x00, 0x01, 0x05, 0x01, 0x01];
305        let md = parse_mgmt_metadata(&data).unwrap();
306        assert_eq!(md.algo, MgmtAlgo::Aes192);
307        assert!(!md.touch_required());
308        assert!(md.is_default);
309    }
310
311    #[test]
312    fn parse_metadata_touch_policy() {
313        let always = [0x01, 0x01, 0x03, 0x02, 0x02, 0x00, 0x02];
314        assert_eq!(
315            parse_mgmt_metadata(&always).unwrap().touch,
316            TouchPolicy::Always
317        );
318        let cached = [0x01, 0x01, 0x03, 0x02, 0x02, 0x00, 0x03];
319        assert!(parse_mgmt_metadata(&cached).unwrap().touch_required());
320    }
321
322    #[test]
323    fn parse_metadata_rejects_unknown_algorithm() {
324        let data = [0x01, 0x01, 0x07];
325        let err = parse_mgmt_metadata(&data).unwrap_err();
326        assert!(err
327            .to_string()
328            .contains("unsupported management key algorithm 0x07"));
329    }
330
331    #[test]
332    fn parse_metadata_requires_algorithm() {
333        assert!(parse_mgmt_metadata(&[0x05, 0x01, 0x01]).is_err());
334    }
335
336    #[test]
337    fn parse_credential_and_slot_metadata() {
338        // PIN metadata: default, 3 tries total, 2 left.
339        let md = parse_credential_metadata(&[0x05, 0x01, 0x01, 0x06, 0x02, 0x03, 0x02]).unwrap();
340        assert_eq!(
341            md,
342            CredentialMetadata {
343                is_default: true,
344                retries_total: 3,
345                retries_left: 2
346            }
347        );
348        assert!(parse_credential_metadata(&[0x05, 0x01, 0x01]).is_err());
349        // Slot metadata: EC P-256, generated on the card.
350        let md = parse_slot_metadata(&[0x01, 0x01, 0x11, 0x03, 0x01, 0x01]).unwrap();
351        assert_eq!(md.algorithm, ALGO_ECCP256);
352        assert_eq!(md.origin, Some(KeyOrigin::Generated));
353        assert_eq!(key_algorithm_name(0x07), "RSA 2048");
354    }
355
356    #[test]
357    fn parse_retries() {
358        // PUK metadata: default flag, retries total=3 remaining=0.
359        let data = [0x05, 0x01, 0x00, 0x06, 0x02, 0x03, 0x00];
360        assert_eq!(parse_retries_remaining(&data), Some(0));
361        assert_eq!(parse_retries_remaining(&[]), None);
362    }
363}