Skip to main content

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