signstar_yubihsm2/automation/
runner.rs1use std::{fmt::Debug, fs::read_to_string, io::Write, path::Path, time::Duration};
4
5use log::info;
6use serde::Serialize;
7use signstar_crypto::passphrase::Passphrase;
8use yubihsm::{
9 Client,
10 Connector,
11 Credentials,
12 authentication,
13 ed25519::Signature,
14 wrap::{self, Message},
15};
16
17use crate::{
18 Error,
19 automation::{Auth, Command},
20 object::{KeyInfo, ObjectId},
21};
22
23fn derive_key_from_file(path: impl AsRef<Path>) -> Result<authentication::Key, Error> {
29 let passphrase = read_to_string(&path).map_err(|source| Error::IoPath {
30 path: path.as_ref().into(),
31 context: "reading key from file",
32 source,
33 })?;
34 let passphrase = Passphrase::new(passphrase);
35 let key = authentication::Key::derive_from_password(passphrase.expose_borrowed().as_bytes());
36 Ok(key)
37}
38
39#[derive(Debug, Serialize)]
41struct Ed25519Signature {
42 r: Vec<u8>,
44 s: Vec<u8>,
46}
47
48impl From<Signature> for Ed25519Signature {
49 fn from(value: Signature) -> Self {
50 Self {
51 r: value.r_bytes().to_vec(),
52 s: value.s_bytes().to_vec(),
53 }
54 }
55}
56
57fn serialize_with_newline(mut writer: &mut dyn Write, object: impl Serialize) -> Result<(), Error> {
65 serde_json::to_writer(&mut writer, &object).map_err(|source| Error::Json {
66 context: "serializing response",
67 source,
68 })?;
69 writer.write_all(b"\n").map_err(|source| Error::Io {
70 context: "writing record delimiter",
71 source,
72 })?;
73 Ok(())
74}
75
76pub struct ScenarioRunner {
78 client: Client,
79}
80
81impl Debug for ScenarioRunner {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("ScenarioRunner").finish()
85 }
86}
87
88impl ScenarioRunner {
89 pub fn new(connector: Connector, auth: Auth) -> Result<Self, Error> {
97 let user = auth.user;
98 let passphrase_file = auth.passphrase_file;
99 let credentials = Credentials::new(user, derive_key_from_file(passphrase_file)?);
100 let client =
101 Client::open(connector, credentials, true).map_err(|source| Error::Client {
102 context: "connecting to client for running a scenario",
103 source,
104 })?;
105 Ok(Self { client })
106 }
107
108 pub fn run_steps(&mut self, steps: &[Command], writer: &mut dyn Write) -> Result<(), Error> {
119 for command in steps.iter() {
120 info!("Executing {command:?}");
121 self.run_command(command, writer)?;
122 }
123 Ok(())
124 }
125
126 fn run_command(&mut self, command: &Command, writer: &mut dyn Write) -> Result<(), Error> {
140 match command {
141 Command::Info => {
142 serialize_with_newline(
143 writer,
144 &self.client.device_info().map_err(|source| Error::Client {
145 context: "executing device info command",
146 source,
147 })?,
148 )?;
149 }
150 Command::Reset => {
151 self.client
152 .reset_device_and_reconnect(Duration::from_secs(2))
153 .map_err(|source| Error::Client {
154 context: "executing device info command",
155 source,
156 })?;
157 }
158 Command::PutAuthKey {
159 info:
160 KeyInfo {
161 key_id,
162 domain,
163 caps,
164 },
165 delegated_caps,
166 passphrase_file,
167 } => {
168 let key = derive_key_from_file(passphrase_file)?;
169 self.client
170 .put_authentication_key(
171 *key_id,
172 Default::default(),
173 (*domain).into(),
174 caps.into(),
175 delegated_caps.into(),
176 Default::default(),
177 key,
178 )
179 .map_err(|source| Error::Client {
180 context: "putting authentication key",
181 source,
182 })?;
183 }
184 Command::GenerateKey {
185 info:
186 KeyInfo {
187 key_id,
188 domain,
189 caps,
190 },
191 } => {
192 self.client
193 .generate_asymmetric_key(
194 *key_id,
195 Default::default(),
196 (*domain).into(),
197 caps.into(),
198 yubihsm::asymmetric::Algorithm::Ed25519,
199 )
200 .map_err(|source| Error::Client {
201 context: "generating asymmetric key",
202 source,
203 })?;
204 }
205 Command::SignEd25519 { key_id, data } => {
206 let sig = self
207 .client
208 .sign_ed25519(*key_id, &data[..])
209 .map_err(|source| Error::Client {
210 context: "signing with ed25519 key",
211 source,
212 })?;
213 serialize_with_newline(writer, Ed25519Signature::from(sig))?;
214 }
215 Command::PutWrapKey {
216 info:
217 KeyInfo {
218 key_id,
219 domain,
220 caps,
221 },
222 delegated_caps,
223 passphrase_file,
224 } => {
225 let key = derive_key_from_file(passphrase_file)?;
226 self.client
227 .put_wrap_key(
228 *key_id,
229 Default::default(),
230 (*domain).into(),
231 caps.into(),
232 delegated_caps.into(),
233 wrap::Algorithm::Aes256Ccm,
234 key.as_secret_slice(),
235 )
236 .map_err(|source| Error::Client {
237 context: "putting wrap key",
238 source,
239 })?;
240 }
241 Command::ExportWrapped {
242 wrap_key_id,
243 object,
244 wrapped_file,
245 } => {
246 let wrapped = self
247 .client
248 .export_wrapped(*wrap_key_id, object.object_type(), object.id())
249 .map_err(|source| Error::Client {
250 context: "exporting wrapped key",
251 source,
252 })?;
253
254 serialize_with_newline(writer, &wrapped)?;
255 std::fs::write(wrapped_file, wrapped.into_vec()).map_err(|source| {
256 Error::IoPath {
257 context: "writing wrapped file",
258 source,
259 path: wrapped_file.into(),
260 }
261 })?;
262 }
263 Command::ImportWrapped {
264 wrap_key_id,
265 wrapped_file,
266 } => {
267 let wrapped = Message::from_vec(std::fs::read(wrapped_file).map_err(|source| {
268 Error::IoPath {
269 context: "reading wrapped file",
270 source,
271 path: wrapped_file.into(),
272 }
273 })?)
274 .map_err(|source| Error::InvalidWrap {
275 context: "reading the wrapped file",
276 source,
277 })?;
278 let imported =
279 self.client
280 .import_wrapped(*wrap_key_id, wrapped)
281 .map_err(|source| Error::Client {
282 context: "importing wrapped key",
283 source,
284 })?;
285
286 serialize_with_newline(writer, ObjectId::from(imported))?;
287 }
288 Command::Auth(Auth {
289 user,
290 passphrase_file,
291 }) => {
292 let credentials = Credentials::new(*user, derive_key_from_file(passphrase_file)?);
293
294 self.client = Client::open(self.client.connector().clone(), credentials, true)
295 .map_err(|source| Error::Client {
296 context: "opening new client",
297 source,
298 })?;
299 }
300 Command::Delete(object) => {
301 self.client
302 .delete_object(object.id(), object.object_type())
303 .map_err(|source| Error::Client {
304 context: "deleting object",
305 source,
306 })?;
307 }
308 Command::GetInfo(object) => {
309 let info = self
310 .client
311 .get_object_info(object.id(), object.object_type())
312 .map_err(|source| Error::Client {
313 context: "getting object info",
314 source,
315 })?;
316
317 serialize_with_newline(writer, &info)?;
318 }
319 Command::ForceAudit(setting) => {
320 self.client
321 .set_force_audit_option((*setting).into())
322 .map_err(|source| Error::Client {
323 context: "setting force audit option",
324 source,
325 })?;
326 }
327 Command::CommandAudit { command, setting } => {
328 self.client
329 .set_command_audit_option(*command, (*setting).into())
330 .map_err(|source| Error::Client {
331 context: "setting command audit option",
332 source,
333 })?;
334 }
335 Command::GetLog => {
336 let log = self
337 .client
338 .get_log_entries()
339 .map_err(|source| Error::Client {
340 context: "getting log entries",
341 source,
342 })?;
343
344 serialize_with_newline(writer, &log)?;
345 }
346 }
347 Ok(())
348 }
349}
350
351#[cfg(all(test, feature = "mockhsm"))]
352mod tests {
353 use std::path::PathBuf;
354 use std::{fs::File, io::stdout};
355
356 use rstest::rstest;
357 use testresult::TestResult;
358
359 use super::*;
360 use crate::automation::Scenario;
361
362 fn run_scenario(scenario_file: impl AsRef<Path>) -> TestResult {
363 let scenario_file = scenario_file.as_ref();
364 eprintln!(
365 "Running scenario file {scenario_file}",
366 scenario_file = scenario_file.display()
367 );
368 let scenario: Scenario = serde_json::from_reader(File::open(scenario_file)?)?;
369 let mut runner = ScenarioRunner::new(Connector::mockhsm(), scenario.auth)?;
370 runner.run_steps(&scenario.steps, &mut stdout())?;
371 Ok(())
372 }
373
374 #[rstest]
375 fn scenario_test(#[files("tests/scenarios/*.json")] scenario_file: PathBuf) -> TestResult {
376 run_scenario(scenario_file)?;
377 Ok(())
378 }
379
380 #[test]
381 fn wrapping_test() -> TestResult {
382 run_scenario("tests/scenarios/wrapping/export-wrapped.json")?;
384 run_scenario("tests/scenarios/wrapping/import-wrapped.json")?;
385 Ok(())
386 }
387}