Skip to main content

yb_core/piv/
mod.rs

1// SPDX-FileCopyrightText: 2025 - 2026 Frederic Ruget <fred@atlant.is> <fred@s3ns.io> (GitHub: @douzebis)
2// SPDX-FileCopyrightText: 2025 - 2026 Thales Cloud Sécurisé
3//
4// SPDX-License-Identifier: MIT
5
6//! PIV backend trait and implementations.
7
8pub mod emulated;
9pub mod hardware;
10pub mod session;
11pub(crate) mod tlv;
12pub mod virtual_piv;
13
14pub use virtual_piv::VirtualPiv;
15
16use anyhow::Result;
17
18// ---------------------------------------------------------------------------
19// FlashHandle — returned by PivBackend::start_flash
20// ---------------------------------------------------------------------------
21
22/// Opaque handle returned by [`PivBackend::start_flash`].
23///
24/// Dropping the handle stops the flash loop on the device.
25pub trait FlashHandle: Send {}
26
27/// No-op flash handle used by backends that do not support LED flashing
28/// (e.g. `VirtualPiv`).
29pub struct NoopFlash;
30impl FlashHandle for NoopFlash {}
31
32// ---------------------------------------------------------------------------
33// DeviceInfo
34// ---------------------------------------------------------------------------
35
36/// Device info returned by list_devices.
37#[derive(Debug, Clone)]
38pub struct DeviceInfo {
39    pub serial: u32,
40    pub version: String,
41    pub reader: String,
42}
43
44/// Abstract PIV backend.  Both HardwarePiv and EmulatedPiv implement this.
45pub trait PivBackend: Send + Sync {
46    /// List connected PC/SC readers.
47    fn list_readers(&self) -> Result<Vec<String>>;
48
49    /// List connected YubiKey devices.
50    fn list_devices(&self) -> Result<Vec<DeviceInfo>>;
51
52    /// Read a PIV data object by its numeric ID.
53    fn read_object(&self, reader: &str, id: u32) -> Result<Vec<u8>>;
54
55    /// Write a PIV data object.
56    ///
57    /// If `management_key` is Some, it is used directly for authentication.
58    /// If `management_key` is None and `pin` is Some, the management key is
59    /// retrieved from the PIN-protected PRINTED object in the same session.
60    fn write_object(
61        &self,
62        reader: &str,
63        id: u32,
64        data: &[u8],
65        management_key: Option<&str>,
66        pin: Option<&str>,
67    ) -> Result<()>;
68
69    /// Verify the user PIN.  Returns Err if verification fails.
70    fn verify_pin(&self, reader: &str, pin: &str) -> Result<()>;
71
72    /// Send a raw APDU and return the response bytes (SW stripped).
73    /// Returns Err if the card returns a non-9000 status.
74    fn send_apdu(&self, reader: &str, apdu: &[u8]) -> Result<Vec<u8>>;
75
76    /// ECDH key agreement: given the peer's uncompressed P-256 point (65 bytes),
77    /// return the shared secret point (65 bytes).
78    fn ecdh(&self, reader: &str, slot: u8, peer_point: &[u8], pin: Option<&str>)
79        -> Result<Vec<u8>>;
80
81    /// ECDSA sign: compute SHA-256(`digest`) and sign it with the P-256 key in
82    /// `slot`.  Returns the raw 64-byte signature `[r (32 bytes) || s (32 bytes)]`.
83    /// PIN is required; pass `None` only if already verified in a prior call.
84    fn ecdsa_sign(
85        &self,
86        reader: &str,
87        slot: u8,
88        digest: &[u8],
89        pin: Option<&str>,
90    ) -> Result<[u8; 64]> {
91        let _ = (reader, slot, digest, pin);
92        anyhow::bail!("ecdsa_sign not implemented for this backend")
93    }
94
95    /// Read the DER-encoded X.509 certificate from a PIV slot.
96    fn read_certificate(&self, reader: &str, slot: u8) -> Result<Vec<u8>>;
97
98    /// Generate an EC P-256 key pair in `slot`; return the public key as an
99    /// uncompressed point (65 bytes).  Requires prior management key auth.
100    fn generate_key(&self, reader: &str, slot: u8, management_key: Option<&str>)
101        -> Result<Vec<u8>>;
102
103    /// Generate an EC P-256 key pair in `slot`, create a self-signed X.509
104    /// certificate with the given subject, and import it into the slot.
105    /// Returns the DER-encoded certificate.
106    fn generate_certificate(
107        &self,
108        reader: &str,
109        slot: u8,
110        subject: &str,
111        management_key: Option<&str>,
112        pin: Option<&str>,
113    ) -> Result<Vec<u8>>;
114
115    /// Read the raw content of the PRINTED object (0x5FC109) after verifying PIN.
116    ///
117    /// Implementations must perform PIN verification and object read in the same
118    /// session to prevent the card from resetting PIN-verified state between calls.
119    fn read_printed_object_with_pin(&self, reader: &str, pin: &str) -> Result<Vec<u8>>;
120
121    /// Replace the management key.
122    ///
123    /// `old_key_hex` is the current management key (48 hex chars for 3DES).
124    /// `new_key_hex` is the replacement key (same length).
125    /// The implementation must authenticate with `old_key_hex` first, then
126    /// issue SET MANAGEMENT KEY to install `new_key_hex`.
127    fn set_management_key(&self, reader: &str, old_key_hex: &str, new_key_hex: &str) -> Result<()>;
128
129    /// Return the size in bytes of a PIV data object, or `None` if the object
130    /// does not exist.  Used by `scan_nvm` to measure NVM usage without writes.
131    /// The default implementation attempts `read_object` and maps "not found"
132    /// errors to `None`; hardware backends should override with an efficient
133    /// implementation that issues a single GET DATA without reading the payload.
134    fn object_size(&self, reader: &str, id: u32) -> Result<Option<usize>> {
135        match self.read_object(reader, id) {
136            Ok(data) => Ok(Some(data.len())),
137            Err(_) => Ok(None),
138        }
139    }
140
141    /// Persist state to a fixture file (no-op for hardware backends).
142    ///
143    /// `VirtualPiv` overrides this to serialize its in-memory state back to
144    /// disk, allowing subprocess tests to share state across process
145    /// boundaries via the `YB_FIXTURE` env var.
146    fn save_fixture(&self, _path: &std::path::Path) -> Result<()> {
147        Ok(())
148    }
149
150    /// Start flashing the LED on the device attached to `reader`.
151    ///
152    /// `on_ms` — how long the LED stays on per cycle (milliseconds).
153    /// `off_ms` — how long the LED stays off per cycle (milliseconds).
154    ///
155    /// Recommended values:
156    /// - Device selection: on=400, off=400 (calm 1.25 Hz, easy to follow)
157    /// - Destructive confirmation: on=200, off=400 (faster, conveys urgency)
158    ///
159    /// Returns a [`FlashHandle`]; the LED stops flashing when the handle is
160    /// dropped.  The default implementation is a no-op so existing backends
161    /// are unaffected.
162    fn start_flash(&self, _reader: &str, _on_ms: u64, _off_ms: u64) -> Box<dyn FlashHandle> {
163        Box::new(NoopFlash)
164    }
165}