Skip to main content

signstar_yubihsm2/
signer.rs

1//! Signing data with YubiHSM.
2
3use signstar_crypto::{
4    Error as SignstarCryptoError,
5    signer::{
6        error::Error as SignstarCryptoSignerError,
7        traits::{RawPublicKey, RawSigningKey},
8    },
9};
10use yubihsm::{
11    Connector,
12    HttpConfig,
13    UsbConfig,
14    asymmetric::Algorithm,
15    client::Client,
16    device::SerialNumber,
17    object::Id,
18};
19
20use crate::{
21    Credentials,
22    Error,
23    connection::{DEFAULT_HTTP_TIMEOUT_MILLIS, YubiHsmUrl},
24};
25
26/// A signing key stored in the YubiHSM.
27pub struct YubiHsm2SigningKey {
28    yubihsm: Client,
29    key_id: Id,
30}
31
32impl YubiHsm2SigningKey {
33    /// Creates a new [`YubiHsm2SigningKey`] for a [`Client`] and the [`Id`] of a signing key.
34    pub fn new(client: Client, id: Id) -> Self {
35        Self {
36            yubihsm: client,
37            key_id: id,
38        }
39    }
40
41    /// Returns the reference for the inner client instance.
42    pub fn client(&self) -> &Client {
43        &self.yubihsm
44    }
45
46    /// Closes the session for the [`YubiHsm2SigningKey`]'s [`Client`].
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if [`Client::close_session`] fails.
51    pub fn close_session(&self) -> Result<(), crate::Error> {
52        self.yubihsm
53            .close_session()
54            .map_err(|source| crate::Error::Client {
55                context: "closing the session for a YubiHSM signing key implementation",
56                source,
57            })
58    }
59
60    /// Returns a signing key emulated in software.
61    ///
62    /// # Warning
63    ///
64    /// The signing key created by this function should be used only for tests as the signing
65    /// material is exposed in memory!
66    ///
67    /// # Errors
68    ///
69    /// When automatic provisioning of the emulator fails this function can return [`Error`].
70    ///
71    /// # Panics
72    ///
73    /// This function panics if certificate generation fails.
74    #[cfg(feature = "_yubihsm2-mockhsm")]
75    pub fn mock(key_id: Id, credentials: &Credentials) -> Result<Self, Error> {
76        use signstar_crypto::traits::UserWithPassphrase as _;
77        use yubihsm::{Credentials as YubiCredentials, authentication};
78
79        use crate::mock::setup_single_key_in_mockhsm;
80
81        let connector = setup_single_key_in_mockhsm(key_id, credentials)?;
82
83        let auth_key = authentication::Key::derive_from_password(
84            credentials.passphrase().expose_borrowed().as_bytes(),
85        );
86        let client = Client::open(
87            connector,
88            YubiCredentials::new(credentials.id(), auth_key),
89            true,
90        )
91        .map_err(|source| Error::Client {
92            context: "connecting to mockhsm",
93            source,
94        })?;
95        Ok(Self::new(client, key_id))
96    }
97
98    /// Returns a new [`YubiHsm2SigningKey`] backed by a remote YubiHSM2.
99    ///
100    /// The target device is identified by the URL.
101    ///
102    /// # Errors
103    ///
104    /// Returns an error, if creating a client connection to the backend fails.
105    pub fn new_remote(
106        url: YubiHsmUrl,
107        key_id: Id,
108        credentials: &Credentials,
109    ) -> Result<Self, Error> {
110        let connector = Connector::http(&HttpConfig {
111            addr: url.host().into(),
112            port: url.port(),
113            tls: url.scheme() == "https",
114            timeout_ms: DEFAULT_HTTP_TIMEOUT_MILLIS,
115        });
116        let client =
117            Client::open(connector, credentials.into(), true).map_err(|source| Error::Client {
118                context: "connecting to a hardware device",
119                source,
120            })?;
121        Ok(Self {
122            yubihsm: client,
123            key_id,
124        })
125    }
126
127    /// Returns a new [`YubiHsm2SigningKey`] backed by specific YubiHSM2 hardware.
128    ///
129    /// The hardware is identified using its `serial_number` and the key is addressed by its
130    /// `key_id`.
131    ///
132    /// # Errors
133    ///
134    /// If the communication with the device fails or the authentication data is incorrect this
135    /// function will return an [`Error`].
136    pub fn new_with_serial_number(
137        serial_number: SerialNumber,
138        key_id: Id,
139        credentials: &Credentials,
140    ) -> Result<Self, Error> {
141        let connector = Connector::usb(&UsbConfig {
142            serial: Some(serial_number),
143            timeout_ms: UsbConfig::DEFAULT_TIMEOUT_MILLIS,
144        });
145        let client =
146            Client::open(connector, credentials.into(), true).map_err(|source| Error::Client {
147                context: "connecting to a hardware device",
148                source,
149            })?;
150        Ok(Self {
151            yubihsm: client,
152            key_id,
153        })
154    }
155}
156
157impl std::fmt::Debug for YubiHsm2SigningKey {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        f.debug_struct("YubiHsm2SigningKey")
160            .field("key_id", &self.key_id)
161            .finish()
162    }
163}
164
165impl RawSigningKey for YubiHsm2SigningKey {
166    /// Returns the internal key identifier formatted as a [`String`].
167    fn key_id(&self) -> String {
168        self.key_id.to_string()
169    }
170
171    /// Signs a raw digest.
172    ///
173    /// The digest is without any framing and the result will be a vector of raw signature parts.
174    ///
175    /// # Errors
176    ///
177    /// If the operation fails the implementation returns a
178    /// [`signstar_crypto::signer::error::Error::Hsm`], which wraps the client-specific HSM error
179    /// in its `source` field.
180    fn sign(&self, digest: &[u8]) -> Result<Vec<Vec<u8>>, SignstarCryptoError> {
181        let sig = self
182            .yubihsm
183            .sign_ed25519(self.key_id, digest)
184            .map_err(|e| SignstarCryptoSignerError::Hsm {
185                context: "calling yubihsm::sign_ed25519",
186                source: Box::new(e),
187            })?;
188
189        Ok(vec![sig.r_bytes().into(), sig.s_bytes().into()])
190    }
191
192    /// Returns certificate bytes associated with this signing key, if any.
193    ///
194    /// This interface does not interpret the certificate in any way but has a notion of certificate
195    /// being set or unset.
196    ///
197    /// # Errors
198    ///
199    /// If the operation fails the implementation returns a
200    /// [`SignstarCryptoSignerError::Hsm`], which wraps the client-specific HSM error
201    /// in its `source` field.
202    fn certificate(&self) -> Result<Option<Vec<u8>>, SignstarCryptoError> {
203        Ok(Some(self.yubihsm.get_opaque(self.key_id).map_err(|e| {
204            SignstarCryptoSignerError::Hsm {
205                context: "retrieving the certificate for a signing key held in a YubiHSM2",
206                source: Box::new(e),
207            }
208        })?))
209    }
210
211    /// Returns raw public parts of this signing key.
212    ///
213    /// Implementation of this trait implies that the signing key exists and as such always has
214    /// public parts. The public key is used for generating application-specific certificates.
215    ///
216    /// # Errors
217    ///
218    /// If the operation fails the implementation returns a
219    /// [`SignstarCryptoSignerError::Hsm`], which wraps the client-specific HSM error
220    /// in its `source` field.
221    fn public(&self) -> Result<RawPublicKey, SignstarCryptoError> {
222        let pk = self.yubihsm.get_public_key(self.key_id).map_err(|e| {
223            SignstarCryptoSignerError::Hsm {
224                context: "retrieving the public key for a signing key held in a YubiHSM2",
225                source: Box::new(e),
226            }
227        })?;
228        if pk.algorithm != Algorithm::Ed25519 {
229            return Err(SignstarCryptoSignerError::InvalidPublicKeyData {
230                context: format!("algorithm of the HSM key {:?} is unsupported", pk.algorithm),
231            }
232            .into());
233        }
234        Ok(RawPublicKey::Ed25519(pk.bytes))
235    }
236}