signstar_yubihsm2/
signer.rs

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