signstar_yubihsm2/
signer.rs

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