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