Skip to main content

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