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