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    UsbConfig,
13    asymmetric::Algorithm,
14    client::Client,
15    device::SerialNumber,
16    object::Id,
17};
18
19use crate::{Credentials, Error};
20
21/// A signing key stored in the YubiHSM.
22pub struct YubiHsm2SigningKey {
23    yubihsm: Client,
24    key_id: Id,
25}
26
27impl YubiHsm2SigningKey {
28    /// Creates a new [`YubiHsm2SigningKey`] for a [`Client`] and the [`Id`] of a signing key.
29    pub fn new(client: Client, id: Id) -> Self {
30        Self {
31            yubihsm: client,
32            key_id: id,
33        }
34    }
35
36    /// Closes the session for the [`YubiHsm2SigningKey`]'s [`Client`].
37    ///
38    /// # Errors
39    ///
40    /// Returns an error if [`Client::close_session`] fails.
41    pub fn close_session(&self) -> Result<(), crate::Error> {
42        self.yubihsm
43            .close_session()
44            .map_err(|source| crate::Error::Client {
45                context: "closing the session for a YubiHSM signing key implementation",
46                source,
47            })
48    }
49
50    /// Returns a signing key emulated in software.
51    ///
52    /// # Warning
53    ///
54    /// The signing key created by this function should be used only for tests as the signing
55    /// material is exposed in memory!
56    ///
57    /// # Errors
58    ///
59    /// When automatic provisioning of the emulator fails this function can return [`Error`].
60    ///
61    /// # Panics
62    ///
63    /// This function panics if certificate generation fails.
64    #[cfg(feature = "_yubihsm2-mockhsm")]
65    pub fn mock(key_id: Id, credentials: &Credentials) -> Result<Self, Error> {
66        use signstar_crypto::{
67            openpgp::{OpenPgpKeyUsageFlags, OpenPgpUserId, OpenPgpVersion},
68            signer::openpgp::{Timestamp, generate_certificate},
69            traits::UserWithPassphrase as _,
70        };
71        use yubihsm::{
72            Capability,
73            Connector,
74            Credentials as YubiCredentials,
75            Domain,
76            asymmetric::Algorithm,
77            authentication,
78            client::Client,
79            opaque,
80        };
81
82        let connector = Connector::mockhsm();
83        let client =
84            Client::open(connector, Default::default(), true).map_err(|source| Error::Client {
85                context: "connecting to mockhsm",
86                source,
87            })?;
88        let auth_key = authentication::Key::derive_from_password(
89            credentials.passphrase().expose_borrowed().as_bytes(),
90        );
91        let domain = Domain::DOM1;
92        client
93            .put_authentication_key(
94                credentials.id(),
95                Default::default(),
96                domain,
97                Capability::empty(),
98                Capability::SIGN_EDDSA,
99                authentication::Algorithm::YubicoAes,
100                auth_key.clone(),
101            )
102            .map_err(|source| Error::Client {
103                context: "putting authentication key",
104                source,
105            })?;
106
107        let client = Client::open(
108            client.connector().clone(),
109            YubiCredentials::new(credentials.id(), auth_key),
110            true,
111        )
112        .map_err(|source| Error::Client {
113            context: "connecting to mockhsm",
114            source,
115        })?;
116
117        client
118            .generate_asymmetric_key(
119                key_id,
120                Default::default(),
121                domain,
122                Capability::SIGN_EDDSA,
123                Algorithm::Ed25519,
124            )
125            .map_err(|source| Error::Client {
126                context: "generating asymmetric key",
127                source,
128            })?;
129
130        let mut flags = OpenPgpKeyUsageFlags::default();
131        flags.set_sign();
132
133        let signer = Self {
134            yubihsm: client,
135            key_id,
136        };
137
138        let cert = generate_certificate(
139            &signer,
140            flags,
141            &[OpenPgpUserId::new("Test".to_owned()).expect("static user ID to be valid")],
142            Default::default(),
143            Timestamp::now(),
144            OpenPgpVersion::V4,
145        )
146        .map_err(|source| Error::CertificateGeneration {
147            context: "generating OpenPGP certificate",
148            source,
149        })?;
150
151        signer
152            .yubihsm
153            .put_opaque(
154                key_id,
155                Default::default(),
156                domain,
157                Capability::empty(),
158                opaque::Algorithm::Data,
159                cert,
160            )
161            .map_err(|source| Error::Client {
162                context: "putting generated certificate on the device",
163                source,
164            })?;
165
166        Ok(signer)
167    }
168
169    /// Returns a new [`YubiHsm2SigningKey`] backed by specific YubiHSM2 hardware.
170    ///
171    /// The hardware is identified using its `serial_number` and the key is addressed by its
172    /// `key_id`.
173    ///
174    /// # Errors
175    ///
176    /// If the communication with the device fails or the authentication data is incorrect this
177    /// function will return an [`Error`].
178    pub fn new_with_serial_number(
179        serial_number: SerialNumber,
180        key_id: Id,
181        credentials: &Credentials,
182    ) -> Result<Self, Error> {
183        let connector = Connector::usb(&UsbConfig {
184            serial: Some(serial_number),
185            timeout_ms: UsbConfig::DEFAULT_TIMEOUT_MILLIS,
186        });
187        let client =
188            Client::open(connector, credentials.into(), true).map_err(|source| Error::Client {
189                context: "connecting to a hardware device",
190                source,
191            })?;
192        Ok(Self {
193            yubihsm: client,
194            key_id,
195        })
196    }
197}
198
199impl std::fmt::Debug for YubiHsm2SigningKey {
200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201        f.debug_struct("YubiHsm2SigningKey")
202            .field("key_id", &self.key_id)
203            .finish()
204    }
205}
206
207impl RawSigningKey for YubiHsm2SigningKey {
208    /// Returns the internal key identifier formatted as a [`String`].
209    fn key_id(&self) -> String {
210        self.key_id.to_string()
211    }
212
213    /// Signs a raw digest.
214    ///
215    /// The digest is without any framing and the result will be a vector of raw signature parts.
216    ///
217    /// # Errors
218    ///
219    /// If the operation fails the implementation returns a
220    /// [`signstar_crypto::signer::error::Error::Hsm`], which wraps the client-specific HSM error
221    /// in its `source` field.
222    fn sign(&self, digest: &[u8]) -> Result<Vec<Vec<u8>>, SignstarCryptoError> {
223        let sig = self
224            .yubihsm
225            .sign_ed25519(self.key_id, digest)
226            .map_err(|e| SignstarCryptoSignerError::Hsm {
227                context: "calling yubihsm::sign_ed25519",
228                source: Box::new(e),
229            })?;
230
231        Ok(vec![sig.r_bytes().into(), sig.s_bytes().into()])
232    }
233
234    /// Returns certificate bytes associated with this signing key, if any.
235    ///
236    /// This interface does not interpret the certificate in any way but has a notion of certificate
237    /// being set or unset.
238    ///
239    /// # Errors
240    ///
241    /// If the operation fails the implementation returns a
242    /// [`SignstarCryptoSignerError::Hsm`], which wraps the client-specific HSM error
243    /// in its `source` field.
244    fn certificate(&self) -> Result<Option<Vec<u8>>, SignstarCryptoError> {
245        Ok(Some(self.yubihsm.get_opaque(self.key_id).map_err(|e| {
246            SignstarCryptoSignerError::Hsm {
247                context: "retrieving the certificate for a signing key held in a YubiHSM2",
248                source: Box::new(e),
249            }
250        })?))
251    }
252
253    /// Returns raw public parts of this signing key.
254    ///
255    /// Implementation of this trait implies that the signing key exists and as such always has
256    /// public parts. The public key is used for generating application-specific certificates.
257    ///
258    /// # Errors
259    ///
260    /// If the operation fails the implementation returns a
261    /// [`SignstarCryptoSignerError::Hsm`], which wraps the client-specific HSM error
262    /// in its `source` field.
263    fn public(&self) -> Result<RawPublicKey, SignstarCryptoError> {
264        let pk = self.yubihsm.get_public_key(self.key_id).map_err(|e| {
265            SignstarCryptoSignerError::Hsm {
266                context: "retrieving the public key for a signing key held in a YubiHSM2",
267                source: Box::new(e),
268            }
269        })?;
270        if pk.algorithm != Algorithm::Ed25519 {
271            return Err(SignstarCryptoSignerError::InvalidPublicKeyData {
272                context: format!("algorithm of the HSM key {:?} is unsupported", pk.algorithm),
273            }
274            .into());
275        }
276        Ok(RawPublicKey::Ed25519(pk.bytes))
277    }
278}