Skip to main content

signstar_host/configure/host/
yubihsm2.rs

1//! Functionality when support for YubiHSM2 backends is compiled in.
2
3use std::str::FromStr;
4
5use log::{debug, error, info, warn};
6use signstar_common::{backend::BackendType, traits::BackendCheck};
7use signstar_config::{
8    admin_credentials::AdminCredentials,
9    config::{UserBackendConnection, UserBackendConnectionFilter},
10    yubihsm2::{
11        YubiHsm2Backend,
12        YubiHsm2Config,
13        admin_credentials::YubiHsm2AdminCredentials,
14        signstar_yubihsm2_export::{Connection, Credentials},
15        yubihsm2_export::{Connector, Id},
16    },
17};
18
19use crate::{
20    Error,
21    configure::{ConfigurationResult, HostConfiguration},
22};
23
24impl<'config> HostConfiguration<'config> {
25    /// Loads [`YubiHsm2AdminCredentials`].
26    ///
27    /// # Errors
28    ///
29    /// Returns an error if [`YubiHsm2AdminCredentials::load`] fails.
30    fn load_yubihsm2_admin_credentials(&self) -> Result<YubiHsm2AdminCredentials, Error> {
31        Ok(
32            YubiHsm2AdminCredentials::load(*self.config().system().admin_secret_handling())
33                .map_err(Box::new)?,
34        )
35    }
36
37    /// Creates new credentials for all non-administrative YubiHSM2 backend users.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error, if the non-administrative credentials for a specific user cannot be
42    /// created.
43    fn create_yubihsm2_non_admin_credentials(&self) -> Result<Vec<Credentials>, Error> {
44        info!("Create new non-administrative user credentials for YubiHSM2.");
45        let user_backend_connections = self.config().user_backend_connections(&[
46            UserBackendConnectionFilter::NonAdmin,
47            UserBackendConnectionFilter::Backend(BackendType::YubiHsm2),
48        ]);
49
50        let credentials_list = {
51            let mut creds_list = Vec::new();
52            for user_backend_connection in user_backend_connections {
53                if let UserBackendConnection::YubiHsm2 { .. } = &user_backend_connection
54                    && let Some(creds_per_user) = user_backend_connection
55                        .create_non_admin_backend_user_secrets()
56                        .map_err(Box::new)?
57                {
58                    for credentials in creds_per_user {
59                        creds_list.push(Credentials::new(
60                            // NOTE: Here we cannot fail, because we already know that
61                            // the user name is valid.
62                            Id::from_str(&credentials.user())?,
63                            credentials.passphrase().clone(),
64                        ));
65                    }
66                }
67            }
68
69            creds_list
70        };
71        debug!(
72            "Created non-administrative credentials for the following IDs: {}",
73            credentials_list
74                .iter()
75                .map(|creds| creds.id().to_string())
76                .collect::<Vec<_>>()
77                .join(", ")
78        );
79
80        Ok(credentials_list)
81    }
82
83    /// Returns the list of available connections to YubiHSM2 backends.
84    fn available_yubihsm2_connections(config: &YubiHsm2Config) -> Vec<Connector> {
85        info!("Query all YubiHSM2 connections for availability.");
86        config
87            .connections()
88            .iter()
89            .filter_map(|connection| {
90                if connection.is_available() {
91                    debug!("Detected available YubiHSM2 connection {connection:?}");
92                    Some(Connector::from(connection))
93                } else {
94                    warn!("Skipping unavailable YubiHSM2 connection {connection:?}");
95                    None
96                }
97            })
98            .collect::<Vec<_>>()
99    }
100
101    /// Returns the list of provisioned YubiHSM2 backend connections.
102    fn provisioned_yubihsm2_connections(config: &YubiHsm2Config) -> Vec<&Connection> {
103        info!("Detect all provisioned YubiHSM2 connections.");
104        config
105            .connections()
106            .iter()
107            .filter(|connection| {
108                if connection.is_provisioned() {
109                    debug!("Detected provisioned YubiHSM2 connection {connection:?}");
110                    true
111                } else {
112                    debug!("Skipping unprovisioned YubiHSM2 connection {connection:?}");
113                    false
114                }
115            })
116            .collect::<Vec<_>>()
117    }
118
119    /// Syncs all available YubiHSM2 backends.
120    ///
121    /// Returns early success, if there is no YubiHSM2 section in the Signstar config.
122    ///
123    /// # Errors
124    ///
125    /// Returns an error, if
126    ///
127    /// - [`YubiHsm2AdminCredentials::load`] cannot load administrative credentials
128    /// - a [`YubiHsm2Backend`] cannot be created for a connection
129    /// - running [`YubiHsm2Backend::sync`] for a specific backend fails
130    fn sync_yubihsm2_backends(
131        &self,
132        available_connections: Vec<Connector>,
133        admin_credentials: &YubiHsm2AdminCredentials,
134        user_credentials: &[Credentials],
135    ) -> Result<(), Error> {
136        let backends = {
137            let mut backends = Vec::new();
138            for nethsm in available_connections.into_iter() {
139                if let Some(backend) =
140                    YubiHsm2Backend::new(nethsm, admin_credentials, self.config())
141                        .map_err(Box::new)?
142                {
143                    backends.push(backend);
144                }
145            }
146            backends
147        };
148
149        for backend in backends {
150            info!(
151                "Sync the state of the Signstar configuration with the YubiHSM2 backend {backend:?}",
152            );
153            backend.sync(user_credentials).map_err(Box::new)?;
154        }
155
156        Ok(())
157    }
158
159    /// Syncs the states of all available YubiHSM2 backends with that of the Signstar configuration.
160    ///
161    /// # Errors
162    ///
163    /// Returns an error, if
164    ///
165    /// - the creation of non-administrative credentials fails
166    /// - the syncing of the backend fails
167    pub fn sync_yubihsm2(&self) -> Result<ConfigurationResult, Error> {
168        info!("Sync the state of the Signstar configuration with all available YubiHSM2 backends.");
169        let Some(yubihsm2_config) = self.config().yubihsm2() else {
170            warn!("There is no YubiHSM2 section in the Signstar configuration. Skipping...");
171            return Ok(ConfigurationResult::MissingConfigurationForBackend);
172        };
173        info!("Found YubiHSM2 section in the Signstar configuration.");
174
175        let available_connections = {
176            let available_connections = Self::available_yubihsm2_connections(yubihsm2_config);
177            if available_connections.is_empty() {
178                error!("There are no available YubiHSM2 connections. Aborting...");
179                return Ok(ConfigurationResult::NoAvailableBackendConnection);
180            }
181
182            available_connections
183        };
184        let provisioned_backends = Self::provisioned_yubihsm2_connections(yubihsm2_config);
185
186        let (admin_credentials, new_admin_credentials) = match self
187            .load_yubihsm2_admin_credentials()
188        {
189            Ok(admin_credentials) => {
190                info!("Found administrative credentials for YubiHSM2.");
191                (admin_credentials, false)
192            }
193            Err(Error::SignstarConfig(signstar_config_error)) => {
194                if !matches!(
195                    *signstar_config_error,
196                    signstar_config::Error::AdminSecretHandling(
197                        signstar_config::admin_credentials::Error::CredsFileMissing { .. },
198                    )
199                ) {
200                    return Err(signstar_config_error.into());
201                }
202
203                if !provisioned_backends.is_empty() {
204                    error!(
205                        "There are provisioned backends, but administrative credentials are not present. You probably want to restore from a backup. Aborting..."
206                    );
207                    return Ok(ConfigurationResult::ProvisionedBackendsButNoAdminCreds);
208                }
209                if available_connections.len() != yubihsm2_config.connections().len() {
210                    error!(
211                        "Not all configured connections are (yet) available ({}/{}) and administrative credentials are not present. Aborting...",
212                        available_connections.len(),
213                        yubihsm2_config.connections().len()
214                    );
215                    return Ok(ConfigurationResult::NotAllConnectionsAvailableAndNoAdminCreds);
216                }
217
218                // All backends are available and unprovisioned.
219                // There are no administrative credentials (yet), so they are created.
220                (
221                    YubiHsm2AdminCredentials::try_from(self.config()).map_err(Box::new)?,
222                    true,
223                )
224            }
225            Err(error) => return Err(error),
226        };
227
228        // Store the admin credentials, if they have been newly created.
229        if new_admin_credentials {
230            admin_credentials
231                .store(*self.config().system().admin_secret_handling())
232                .map_err(Box::new)?;
233        }
234
235        // Non-administrative credentials are always created from scratch, unconditionally.
236        let user_credentials = self.create_yubihsm2_non_admin_credentials()?;
237
238        self.sync_yubihsm2_backends(
239            available_connections,
240            &admin_credentials,
241            user_credentials.as_slice(),
242        )?;
243
244        Ok(ConfigurationResult::SyncSucceeded)
245    }
246}