signstar_yubihsm2/automation/
runner.rs1use std::{fmt::Debug, fs::read_to_string, path::Path};
4#[cfg(feature = "serde")]
5use std::{io::Write, time::Duration};
6
7#[cfg(feature = "serde")]
8use log::info;
9#[cfg(feature = "serde")]
10use serde::Serialize;
11use signstar_crypto::passphrase::Passphrase;
12#[cfg(feature = "serde")]
13use yubihsm::wrap::{self, Message};
14use yubihsm::{Client, Connector, Credentials, authentication, ed25519::Signature};
15
16use crate::{Error, automation::Auth};
17#[cfg(feature = "serde")]
18use crate::{
19 automation::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)]
41#[cfg_attr(feature = "serde", derive(Serialize))]
42#[cfg_attr(
43 any(
44 all(not(feature = "serde"), feature = "_yubihsm2-mockhsm"),
45 all(
46 not(feature = "serde"),
47 not(feature = "_yubihsm2-mockhsm"),
48 not(feature = "cli")
49 )
50 ),
51 allow(unused)
52)]
53struct Ed25519Signature {
54 r: Vec<u8>,
56 s: Vec<u8>,
58}
59
60impl From<Signature> for Ed25519Signature {
61 fn from(value: Signature) -> Self {
62 Self {
63 r: value.r_bytes().to_vec(),
64 s: value.s_bytes().to_vec(),
65 }
66 }
67}
68
69#[cfg(feature = "serde")]
77fn serialize_with_newline(mut writer: &mut dyn Write, object: impl Serialize) -> Result<(), Error> {
78 serde_json::to_writer(&mut writer, &object).map_err(|source| Error::Json {
79 context: "serializing response",
80 source,
81 })?;
82 writer.write_all(b"\n").map_err(|source| Error::Io {
83 context: "writing record delimiter",
84 source,
85 })?;
86 Ok(())
87}
88
89pub struct ScenarioRunner {
91 #[cfg_attr(
92 any(
93 all(not(feature = "serde"), feature = "_yubihsm2-mockhsm"),
94 all(
95 not(feature = "serde"),
96 not(feature = "_yubihsm2-mockhsm"),
97 not(feature = "cli")
98 )
99 ),
100 allow(unused)
101 )]
102 client: Client,
103}
104
105impl Debug for ScenarioRunner {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 f.debug_struct("ScenarioRunner").finish()
109 }
110}
111
112impl ScenarioRunner {
113 pub fn new(connector: Connector, auth: Auth) -> Result<Self, Error> {
121 let user = auth.user;
122 let passphrase_file = auth.passphrase_file;
123 let credentials = Credentials::new(user.into(), derive_key_from_file(passphrase_file)?);
124 let client =
125 Client::open(connector, credentials, true).map_err(|source| Error::Client {
126 context: "connecting to client for running a scenario",
127 source,
128 })?;
129 Ok(Self { client })
130 }
131
132 #[cfg(feature = "serde")]
143 pub fn run_steps(&mut self, steps: &[Command], writer: &mut dyn Write) -> Result<(), Error> {
144 for command in steps.iter() {
145 info!("Executing {command:?}");
146 self.run_command(command, writer)?;
147 }
148 Ok(())
149 }
150
151 #[cfg(feature = "serde")]
165 fn run_command(&mut self, command: &Command, writer: &mut dyn Write) -> Result<(), Error> {
166 match command {
167 Command::Info => {
168 serialize_with_newline(
169 writer,
170 &self.client.device_info().map_err(|source| Error::Client {
171 context: "executing device info command",
172 source,
173 })?,
174 )?;
175 }
176 Command::Reset => {
177 self.client
178 .reset_device_and_reconnect(Duration::from_secs(2))
179 .map_err(|source| Error::Client {
180 context: "executing device info command",
181 source,
182 })?;
183 }
184 Command::PutAuthKey {
185 info:
186 KeyInfo {
187 key_id,
188 domains,
189 caps,
190 },
191 delegated_caps,
192 passphrase_file,
193 } => {
194 let key = derive_key_from_file(passphrase_file)?;
195 self.client
196 .put_authentication_key(
197 key_id.into(),
198 Default::default(),
199 domains.into(),
200 caps.into(),
201 delegated_caps.into(),
202 Default::default(),
203 key,
204 )
205 .map_err(|source| Error::Client {
206 context: "putting authentication key",
207 source,
208 })?;
209 }
210 Command::GenerateKey {
211 info:
212 KeyInfo {
213 key_id,
214 domains,
215 caps,
216 },
217 } => {
218 self.client
219 .generate_asymmetric_key(
220 key_id.into(),
221 Default::default(),
222 domains.into(),
223 caps.into(),
224 yubihsm::asymmetric::Algorithm::Ed25519,
225 )
226 .map_err(|source| Error::Client {
227 context: "generating asymmetric key",
228 source,
229 })?;
230 }
231 Command::SignEd25519 { key_id, data } => {
232 let sig = self
233 .client
234 .sign_ed25519(key_id.into(), &data[..])
235 .map_err(|source| Error::Client {
236 context: "signing with ed25519 key",
237 source,
238 })?;
239 serialize_with_newline(writer, Ed25519Signature::from(sig))?;
240 }
241 Command::PutWrapKey {
242 info:
243 KeyInfo {
244 key_id,
245 domains,
246 caps,
247 },
248 delegated_caps,
249 passphrase_file,
250 } => {
251 let key = derive_key_from_file(passphrase_file)?;
252 self.client
253 .put_wrap_key(
254 key_id.into(),
255 Default::default(),
256 domains.into(),
257 caps.into(),
258 delegated_caps.into(),
259 wrap::Algorithm::Aes256Ccm,
260 key.as_secret_slice(),
261 )
262 .map_err(|source| Error::Client {
263 context: "putting wrap key",
264 source,
265 })?;
266 }
267 Command::ExportWrapped {
268 wrap_key_id,
269 object,
270 wrapped_file,
271 } => {
272 let wrapped = self
273 .client
274 .export_wrapped(wrap_key_id.into(), object.object_type(), object.id().into())
275 .map_err(|source| Error::Client {
276 context: "exporting wrapped key",
277 source,
278 })?;
279
280 serialize_with_newline(writer, &wrapped)?;
281 std::fs::write(wrapped_file, wrapped.into_vec()).map_err(|source| {
282 Error::IoPath {
283 context: "writing wrapped file",
284 source,
285 path: wrapped_file.into(),
286 }
287 })?;
288 }
289 Command::ImportWrapped {
290 wrap_key_id,
291 wrapped_file,
292 } => {
293 let wrapped = Message::from_vec(std::fs::read(wrapped_file).map_err(|source| {
294 Error::IoPath {
295 context: "reading wrapped file",
296 source,
297 path: wrapped_file.into(),
298 }
299 })?)
300 .map_err(|source| Error::InvalidWrap {
301 context: "reading the wrapped file",
302 source,
303 })?;
304 let imported = self
305 .client
306 .import_wrapped(wrap_key_id.into(), wrapped)
307 .map_err(|source| Error::Client {
308 context: "importing wrapped key",
309 source,
310 })?;
311
312 serialize_with_newline(writer, ObjectId::try_from(imported)?)?;
313 }
314 Command::Auth(Auth {
315 user,
316 passphrase_file,
317 }) => {
318 let credentials =
319 Credentials::new(user.into(), derive_key_from_file(passphrase_file)?);
320
321 self.client = Client::open(self.client.connector().clone(), credentials, true)
322 .map_err(|source| Error::Client {
323 context: "opening new client",
324 source,
325 })?;
326 }
327 Command::Delete(object) => {
328 self.client
329 .delete_object(object.id().into(), object.object_type())
330 .map_err(|source| Error::Client {
331 context: "deleting object",
332 source,
333 })?;
334 }
335 Command::GetInfo(object) => {
336 let info = self
337 .client
338 .get_object_info(object.id().into(), object.object_type())
339 .map_err(|source| Error::Client {
340 context: "getting object info",
341 source,
342 })?;
343
344 serialize_with_newline(writer, &info)?;
345 }
346 Command::ForceAudit(setting) => {
347 self.client
348 .set_force_audit_option((*setting).into())
349 .map_err(|source| Error::Client {
350 context: "setting force audit option",
351 source,
352 })?;
353 }
354 Command::CommandAudit { command, setting } => {
355 self.client
356 .set_command_audit_option(*command, (*setting).into())
357 .map_err(|source| Error::Client {
358 context: "setting command audit option",
359 source,
360 })?;
361 }
362 Command::GetLog => {
363 let log = self
364 .client
365 .get_log_entries()
366 .map_err(|source| Error::Client {
367 context: "getting log entries",
368 source,
369 })?;
370
371 serialize_with_newline(writer, &log)?;
372 }
373 }
374 Ok(())
375 }
376}
377
378#[cfg(test)]
379mod tests {
380
381 use super::*;
382
383 #[test]
384 fn ed25519_signature() {
385 let signature = Ed25519Signature {
386 r: vec![],
387 s: vec![],
388 };
389
390 println!("r: {:?}, s: {:?}", signature.r, signature.s);
391 }
392
393 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
394 mod scenario {
395 use std::path::PathBuf;
396 use std::{fs::File, io::stdout};
397
398 use rstest::rstest;
399 use testresult::TestResult;
400
401 use super::*;
402 use crate::automation::Scenario;
403
404 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
405 fn run_scenario(scenario_file: impl AsRef<Path>) -> TestResult {
406 let scenario_file = scenario_file.as_ref();
407 eprintln!(
408 "Running scenario file {scenario_file}",
409 scenario_file = scenario_file.display()
410 );
411 let scenario: Scenario = serde_json::from_reader(File::open(scenario_file)?)?;
412 let mut runner = ScenarioRunner::new(Connector::mockhsm(), scenario.auth)?;
413 runner.run_steps(&scenario.steps, &mut stdout())?;
414 Ok(())
415 }
416
417 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
418 #[rstest]
419 fn scenario_test(#[files("tests/scenarios/*.json")] scenario_file: PathBuf) -> TestResult {
420 run_scenario(scenario_file)?;
421 Ok(())
422 }
423
424 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
425 #[test]
426 fn wrapping_test() -> TestResult {
427 run_scenario("tests/scenarios/wrapping/export-wrapped.json")?;
429 run_scenario("tests/scenarios/wrapping/import-wrapped.json")?;
430 Ok(())
431 }
432 }
433}