Skip to main content

lager/nets/
hsm.rs

1//! YubiHSM 2 nets (box >= 0.59.0): key generation, signing and object
2//! management on a hardware security module attached to the box.
3//!
4//! Every action except [`Hsm::info`] logs in to the HSM, with an
5//! [`HsmAuth`]. The box never logs or echoes the password, and `HsmAuth`'s
6//! `Debug` output leaves it out.
7//!
8//! ```no_run
9//! # #[cfg(feature = "blocking")]
10//! # fn demo() -> lager::Result<()> {
11//! use lager::{GenerateKey, HsmAuth, LagerBox};
12//!
13//! let lager = LagerBox::from_env()?;
14//! let hsm = lager.hsm("hsm1");
15//! let auth = HsmAuth::new(std::env::var("HSM_PASSWORD").unwrap());
16//! let key = hsm.generate(&auth, &GenerateKey::new("ec-p256"))?;
17//! let signature = hsm.sign(&auth, key.id, b"firmware digest", "ecdsa-sha256")?;
18//! println!("{}\n{} signature bytes", key.public_key_pem, signature.len());
19//! # Ok(())
20//! # }
21//! # fn main() {}
22//! ```
23//!
24//! An unknown algorithm, mechanism or object type fails with
25//! [`crate::Error::Box`] (HTTP 400) listing the valid names. A failed login
26//! or an HSM error is HTTP 502.
27
28use std::fmt;
29
30use serde::Deserialize;
31
32use super::net_handle;
33
34/// Login credentials for an HSM action: an authentication key ID and its
35/// password. `Debug` output omits the password.
36#[derive(Clone)]
37pub struct HsmAuth {
38    auth_key_id: u16,
39    password: String,
40}
41
42impl HsmAuth {
43    /// Log in with authentication key 1, the factory default key.
44    pub fn new(password: impl Into<String>) -> Self {
45        HsmAuth::with_key(1, password)
46    }
47
48    /// Log in with the given authentication key.
49    pub fn with_key(auth_key_id: u16, password: impl Into<String>) -> Self {
50        HsmAuth {
51            auth_key_id,
52            password: password.into(),
53        }
54    }
55
56    /// The authentication key this logs in with.
57    pub fn auth_key_id(&self) -> u16 {
58        self.auth_key_id
59    }
60}
61
62impl fmt::Debug for HsmAuth {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        f.debug_struct("HsmAuth")
65            .field("auth_key_id", &self.auth_key_id)
66            .field("password", &"<redacted>")
67            .finish()
68    }
69}
70
71/// The device's identity, from [`Hsm::info`].
72#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
73#[non_exhaustive]
74pub struct HsmInfo {
75    /// Firmware version (`"2.4.0"`).
76    pub firmware: String,
77    /// Serial number.
78    pub serial: u64,
79    /// Audit-log entries in use.
80    pub log_used: u32,
81    /// Audit-log capacity.
82    pub log_size: u32,
83    /// Part number, when the device reports one.
84    #[serde(default)]
85    pub part_number: Option<String>,
86    /// Algorithms the device supports (`"ec-p256"`, `"ed25519"`, ...).
87    #[serde(default)]
88    pub algorithms: Vec<String>,
89}
90
91/// One object stored on the HSM, from [`Hsm::objects`].
92#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
93#[non_exhaustive]
94pub struct HsmObject {
95    /// Object ID.
96    pub id: u16,
97    /// Object type (`"asymmetric-key"`, `"authentication-key"`, ...).
98    #[serde(rename = "type")]
99    pub object_type: String,
100    /// Label.
101    #[serde(default)]
102    pub label: String,
103    /// Algorithm, for a key.
104    #[serde(default)]
105    pub algorithm: Option<String>,
106    /// Domains the object belongs to (1-16).
107    #[serde(default)]
108    pub domains: Vec<u8>,
109    /// Capabilities (`"sign-ecdsa"`, `"exportable-under-wrap"`, ...).
110    #[serde(default)]
111    pub capabilities: Vec<String>,
112    /// Size in bytes.
113    #[serde(default)]
114    pub size: u32,
115    /// Sequence number: how many times this ID has been reused.
116    #[serde(default)]
117    pub sequence: u32,
118}
119
120/// What [`Hsm::generate`] creates.
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct GenerateKey {
123    /// Algorithm: `"ec-p256"`, `"ec-p384"`, `"ec-p521"`, `"ec-k256"` or
124    /// `"ed25519"`. The box refuses RSA, whose generation can take minutes.
125    pub algorithm: String,
126    /// Object ID to store the key under; 0 lets the HSM pick one.
127    pub object_id: u16,
128    /// Label.
129    pub label: String,
130    /// Domains (1-16); empty means domain 1.
131    pub domains: Vec<u8>,
132    /// Capabilities; empty means the box's defaults for the algorithm.
133    pub capabilities: Vec<String>,
134}
135
136impl GenerateKey {
137    /// A key of this algorithm, with every other setting at its default.
138    pub fn new(algorithm: impl Into<String>) -> Self {
139        GenerateKey {
140            algorithm: algorithm.into(),
141            object_id: 0,
142            label: String::new(),
143            domains: Vec::new(),
144            capabilities: Vec::new(),
145        }
146    }
147}
148
149/// A key [`Hsm::generate`] created.
150#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
151#[non_exhaustive]
152pub struct GeneratedKey {
153    /// The key's object ID.
154    pub id: u16,
155    /// Its public key, as PEM (SubjectPublicKeyInfo).
156    pub public_key_pem: String,
157}
158
159pub(crate) mod ops {
160    use std::time::Duration;
161
162    use serde::Deserialize;
163    use serde_json::{json, Value};
164
165    use super::{GenerateKey, GeneratedKey, HsmAuth, HsmInfo, HsmObject};
166    use crate::error::{Error, Result};
167    use crate::nets::debug::{base64_decode, base64_encode};
168    use crate::wire::{net_command, unit, value_as, CommandResponse, Op, Timeout};
169
170    const ROLE: &str = "hsm";
171
172    /// Each action opens the device, logs in and runs one operation.
173    fn budget() -> Timeout {
174        Timeout::After(Duration::from_secs(30))
175    }
176
177    /// `params` plus the login fields.
178    fn with_auth(auth: &HsmAuth, mut params: Value) -> Value {
179        params["auth_key_id"] = json!(auth.auth_key_id);
180        params["password"] = json!(auth.password);
181        params
182    }
183
184    fn cmd<T>(
185        name: &str,
186        action: &str,
187        params: Value,
188        timeout: Timeout,
189        parse: fn(CommandResponse) -> Result<T>,
190    ) -> Op<T> {
191        Op {
192            req: net_command(name, ROLE, action, params, timeout),
193            parse,
194        }
195    }
196
197    fn parse_string(resp: CommandResponse) -> Result<String> {
198        match resp.value {
199            Some(Value::String(s)) => Ok(s),
200            other => Err(Error::Decode(format!(
201                "expected string 'value', got {other:?}"
202            ))),
203        }
204    }
205
206    #[derive(Deserialize)]
207    struct Signature {
208        signature: String,
209    }
210
211    fn parse_signature(resp: CommandResponse) -> Result<Vec<u8>> {
212        let sig: Signature = value_as(resp)?;
213        base64_decode(&sig.signature)
214            .ok_or_else(|| Error::Decode("'signature' is not valid base64".to_string()))
215    }
216
217    pub(crate) fn info(name: &str) -> Op<HsmInfo> {
218        cmd(name, "info", json!({}), budget(), value_as::<HsmInfo>)
219    }
220
221    pub(crate) fn blink(name: &str, auth: &HsmAuth, seconds: u8) -> Op<()> {
222        cmd(
223            name,
224            "blink",
225            with_auth(auth, json!({ "seconds": seconds })),
226            // The box waits out the blink before answering.
227            Timeout::After(Duration::from_secs(u64::from(seconds) + 30)),
228            unit,
229        )
230    }
231
232    pub(crate) fn objects(
233        name: &str,
234        auth: &HsmAuth,
235        object_type: Option<&str>,
236        label: Option<&str>,
237    ) -> Op<Vec<HsmObject>> {
238        let mut params = json!({});
239        if let Some(t) = object_type {
240            params["type"] = json!(t);
241        }
242        if let Some(l) = label {
243            params["label"] = json!(l);
244        }
245        cmd(
246            name,
247            "objects",
248            with_auth(auth, params),
249            budget(),
250            value_as::<Vec<HsmObject>>,
251        )
252    }
253
254    pub(crate) fn delete(
255        name: &str,
256        auth: &HsmAuth,
257        object_id: u16,
258        object_type: Option<&str>,
259    ) -> Op<()> {
260        let mut params = json!({ "object_id": object_id });
261        if let Some(t) = object_type {
262            params["object_type"] = json!(t);
263        }
264        cmd(name, "delete", with_auth(auth, params), budget(), unit)
265    }
266
267    pub(crate) fn generate(name: &str, auth: &HsmAuth, key: &GenerateKey) -> Op<GeneratedKey> {
268        let mut params = json!({
269            "algorithm": key.algorithm,
270            "object_id": key.object_id,
271            "label": key.label,
272        });
273        if !key.domains.is_empty() {
274            params["domains"] = json!(key.domains);
275        }
276        if !key.capabilities.is_empty() {
277            params["capabilities"] = json!(key.capabilities);
278        }
279        cmd(
280            name,
281            "generate",
282            with_auth(auth, params),
283            budget(),
284            value_as::<GeneratedKey>,
285        )
286    }
287
288    pub(crate) fn public_key(name: &str, auth: &HsmAuth, object_id: u16) -> Op<String> {
289        cmd(
290            name,
291            "pubkey",
292            with_auth(auth, json!({ "object_id": object_id })),
293            budget(),
294            parse_string,
295        )
296    }
297
298    pub(crate) fn sign(
299        name: &str,
300        auth: &HsmAuth,
301        object_id: u16,
302        data: &[u8],
303        mechanism: &str,
304    ) -> Op<Vec<u8>> {
305        let params = json!({
306            "object_id": object_id,
307            "data": base64_encode(data),
308            "mechanism": mechanism,
309        });
310        cmd(
311            name,
312            "sign",
313            with_auth(auth, params),
314            budget(),
315            parse_signature,
316        )
317    }
318
319    pub(crate) fn reset(name: &str, auth: &HsmAuth) -> Op<()> {
320        cmd(
321            name,
322            "reset",
323            with_auth(auth, json!({})),
324            // The box allows the reboot 60 s.
325            Timeout::After(Duration::from_secs(75)),
326            unit,
327        )
328    }
329}
330
331net_handle! {
332    /// Handle for a YubiHSM 2 net. See the [module docs](self).
333    sync: Hsm,
334    async: AsyncHsm,
335    methods: {
336        /// Firmware, serial number, audit-log use and supported algorithms.
337        /// Needs no login.
338        fn info() -> HsmInfo = ops::info;
339        /// Blink the device's LED for `seconds`, to find it on the bench.
340        fn blink(auth: &HsmAuth, seconds: u8) -> () = ops::blink;
341        /// Stored objects, optionally only one type (`"asymmetric-key"`, ...)
342        /// or label.
343        fn objects(auth: &HsmAuth, object_type: Option<&str>, label: Option<&str>) -> Vec<HsmObject> = ops::objects;
344        /// Delete an object. `object_type` defaults to `"asymmetric-key"`.
345        fn delete(auth: &HsmAuth, object_id: u16, object_type: Option<&str>) -> () = ops::delete;
346        /// Generate an asymmetric key on the device and return its public
347        /// key.
348        fn generate(auth: &HsmAuth, key: &GenerateKey) -> GeneratedKey = ops::generate;
349        /// A stored key's public key, as PEM.
350        fn public_key(auth: &HsmAuth, object_id: u16) -> String = ops::public_key;
351        /// Sign `data` with a stored key and return the signature.
352        /// `mechanism` is one of `"ecdsa-sha256"`, `"ecdsa-sha384"`,
353        /// `"ecdsa-sha512"`, `"eddsa"`, `"rsa-pkcs1-sha256"` (and `-sha384`,
354        /// `-sha512`) or `"rsa-pss-sha256"` (and `-sha384`, `-sha512`).
355        fn sign(auth: &HsmAuth, object_id: u16, data: &[u8], mechanism: &str) -> Vec<u8> = ops::sign;
356        /// **Factory-reset the device.** Deletes every key and object and
357        /// restores the factory login (key 1, password `password`). Cannot
358        /// be undone.
359        fn reset(auth: &HsmAuth) -> () = ops::reset;
360    }
361}