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}