signstar_yubihsm2/object/
key.rs1use std::{collections::BTreeSet, fmt::Display, fs::read_to_string, hash::Hash, path::Path};
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7#[cfg(feature = "serde")]
8use serde_repr::{Deserialize_repr, Serialize_repr};
9use signstar_crypto::passphrase::{Passphrase, PassphrasePolicy};
10use strum::{AsRefStr, IntoStaticStr};
11use yubihsm::authentication::Key as YubiHsmAuthenticationKey;
12
13use crate::object::{Capabilities, Id};
14
15#[derive(
20 AsRefStr,
21 Clone,
22 Copy,
23 Debug,
24 strum::Display,
25 Eq,
26 Hash,
27 IntoStaticStr,
28 Ord,
29 PartialEq,
30 PartialOrd,
31)]
32#[cfg_attr(feature = "serde", derive(Deserialize_repr, Serialize_repr))]
33#[repr(u8)]
34pub enum Domain {
35 #[strum(serialize = "1")]
37 One = 1,
38 #[strum(serialize = "2")]
40 Two = 2,
41 #[strum(serialize = "3")]
43 Three = 3,
44 #[strum(serialize = "4")]
46 Four = 4,
47 #[strum(serialize = "5")]
49 Five = 5,
50 #[strum(serialize = "6")]
52 Six = 6,
53 #[strum(serialize = "7")]
55 Seven = 7,
56 #[strum(serialize = "8")]
58 Eight = 8,
59 #[strum(serialize = "9")]
61 Nine = 9,
62 #[strum(serialize = "10")]
64 Ten = 10,
65 #[strum(serialize = "11")]
67 Eleven = 11,
68 #[strum(serialize = "12")]
70 Twelve = 12,
71 #[strum(serialize = "13")]
73 Thirteen = 13,
74 #[strum(serialize = "14")]
76 Fourteen = 14,
77 #[strum(serialize = "15")]
79 Fifteen = 15,
80 #[strum(serialize = "16")]
82 Sixteen = 16,
83}
84
85#[cfg(feature = "cli")]
86impl clap::ValueEnum for Domain {
87 fn value_variants<'a>() -> &'a [Self] {
88 static VARIANTS: &[Domain] = &[
89 Domain::One,
90 Domain::Two,
91 Domain::Three,
92 Domain::Four,
93 Domain::Five,
94 Domain::Six,
95 Domain::Seven,
96 Domain::Eight,
97 Domain::Nine,
98 Domain::Ten,
99 Domain::Eleven,
100 Domain::Twelve,
101 Domain::Thirteen,
102 Domain::Fourteen,
103 Domain::Fifteen,
104 Domain::Sixteen,
105 ];
106 VARIANTS
107 }
108
109 fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
110 let str: &'static str = self.into();
111 Some(clap::builder::PossibleValue::new(str))
112 }
113}
114
115impl From<Domain> for yubihsm::Domain {
116 fn from(value: Domain) -> Self {
117 match value {
118 Domain::One => Self::DOM1,
119 Domain::Two => Self::DOM2,
120 Domain::Three => Self::DOM3,
121 Domain::Four => Self::DOM4,
122 Domain::Five => Self::DOM5,
123 Domain::Six => Self::DOM6,
124 Domain::Seven => Self::DOM7,
125 Domain::Eight => Self::DOM8,
126 Domain::Nine => Self::DOM9,
127 Domain::Ten => Self::DOM10,
128 Domain::Eleven => Self::DOM11,
129 Domain::Twelve => Self::DOM12,
130 Domain::Thirteen => Self::DOM13,
131 Domain::Fourteen => Self::DOM14,
132 Domain::Fifteen => Self::DOM15,
133 Domain::Sixteen => Self::DOM16,
134 }
135 }
136}
137
138#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
142#[cfg_attr(
143 feature = "serde",
144 derive(Serialize, Deserialize),
145 serde(try_from = "BTreeSet<Domain>")
146)]
147pub struct Domains(BTreeSet<Domain>);
148
149impl Domains {
150 pub fn to_be_bytes(&self) -> [u8; 2] {
152 self.bits().to_be_bytes()
153 }
154
155 pub fn all() -> Self {
157 yubihsm::Domain::all().bits().into()
158 }
159
160 pub fn bits(&self) -> u16 {
162 yubihsm::Domain::from(self).bits()
163 }
164}
165
166impl Display for Domains {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 write!(
173 f,
174 "{}",
175 self.0
176 .iter()
177 .map(|domain| domain.as_ref())
178 .collect::<Vec<_>>()
179 .join(", ")
180 )
181 }
182}
183
184impl From<Domain> for Domains {
185 fn from(value: Domain) -> Self {
186 Self(BTreeSet::from_iter([value]))
187 }
188}
189
190impl From<yubihsm::Domain> for Domains {
191 fn from(value: yubihsm::Domain) -> Self {
192 let lookup = [
193 (yubihsm::Domain::DOM1, Domain::One),
194 (yubihsm::Domain::DOM2, Domain::Two),
195 (yubihsm::Domain::DOM3, Domain::Three),
196 (yubihsm::Domain::DOM4, Domain::Four),
197 (yubihsm::Domain::DOM5, Domain::Five),
198 (yubihsm::Domain::DOM6, Domain::Six),
199 (yubihsm::Domain::DOM7, Domain::Seven),
200 (yubihsm::Domain::DOM8, Domain::Eight),
201 (yubihsm::Domain::DOM9, Domain::Nine),
202 (yubihsm::Domain::DOM10, Domain::Ten),
203 (yubihsm::Domain::DOM11, Domain::Eleven),
204 (yubihsm::Domain::DOM12, Domain::Twelve),
205 (yubihsm::Domain::DOM13, Domain::Thirteen),
206 (yubihsm::Domain::DOM14, Domain::Fourteen),
207 (yubihsm::Domain::DOM15, Domain::Fifteen),
208 (yubihsm::Domain::DOM16, Domain::Sixteen),
209 ];
210
211 Domains(BTreeSet::from_iter(lookup.iter().filter_map(
212 |(yubi_dom, dom)| {
213 if value.contains(*yubi_dom) {
214 Some(*dom)
215 } else {
216 None
217 }
218 },
219 )))
220 }
221}
222
223impl From<u16> for Domains {
224 fn from(value: u16) -> Self {
225 yubihsm::Domain::from_bits_retain(value).into()
226 }
227}
228
229impl From<&[Domain]> for Domains {
230 fn from(value: &[Domain]) -> Self {
231 Self(value.iter().copied().collect())
232 }
233}
234
235impl TryFrom<BTreeSet<Domain>> for Domains {
236 type Error = crate::object::Error;
237
238 fn try_from(domains: BTreeSet<Domain>) -> Result<Self, Self::Error> {
239 if domains.is_empty() {
240 return Err(Self::Error::EmptySetOfDomains);
241 }
242 Ok(Self(domains))
243 }
244}
245
246impl From<&Domains> for yubihsm::Domain {
247 fn from(value: &Domains) -> Self {
248 value
249 .0
250 .iter()
251 .map(|cap| yubihsm::Domain::from(*cap))
252 .fold(yubihsm::Domain::empty(), |acc, c| acc | c)
253 }
254}
255
256#[derive(Debug)]
258pub struct AuthenticationKey(YubiHsmAuthenticationKey);
259
260impl AuthenticationKey {
261 pub const PASSPHRASE_POLICY: PassphrasePolicy = PassphrasePolicy { minimum_length: 30 };
263}
264
265impl AsRef<YubiHsmAuthenticationKey> for AuthenticationKey {
266 fn as_ref(&self) -> &YubiHsmAuthenticationKey {
267 &self.0
268 }
269}
270
271impl From<AuthenticationKey> for YubiHsmAuthenticationKey {
272 fn from(value: AuthenticationKey) -> Self {
273 value.0
274 }
275}
276
277impl From<&AuthenticationKey> for YubiHsmAuthenticationKey {
278 fn from(value: &AuthenticationKey) -> Self {
279 value.0.clone()
280 }
281}
282
283impl TryFrom<&Path> for AuthenticationKey {
284 type Error = crate::Error;
285
286 fn try_from(file: &Path) -> Result<Self, Self::Error> {
298 let passphrase = Passphrase::new_with_policy(
299 read_to_string(file).map_err(|source| crate::Error::IoPath {
300 path: file.into(),
301 context: "reading the passphrase for an authentication key derivation from file",
302 source,
303 })?,
304 &Self::PASSPHRASE_POLICY,
305 )?;
306
307 Ok(Self(YubiHsmAuthenticationKey::derive_from_password(
308 passphrase.expose_borrowed().as_bytes(),
309 )))
310 }
311}
312
313impl TryFrom<&Passphrase> for AuthenticationKey {
314 type Error = crate::Error;
315
316 fn try_from(passphrase: &Passphrase) -> Result<Self, Self::Error> {
324 passphrase.check_against_policy(&Self::PASSPHRASE_POLICY)?;
325
326 Ok(Self(YubiHsmAuthenticationKey::derive_from_password(
327 passphrase.expose_borrowed().as_bytes(),
328 )))
329 }
330}
331
332#[derive(Clone, Debug)]
337#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
338pub struct KeyInfo {
339 pub key_id: Id,
341
342 pub domains: Domains,
347
348 pub caps: Capabilities,
350}
351
352#[cfg(test)]
353mod tests {
354 use std::io::Write;
355
356 use rand::{
357 distributions::{Alphanumeric, DistString},
358 thread_rng,
359 };
360 use tempfile::{NamedTempFile, TempDir};
361 use testresult::TestResult;
362
363 use super::*;
364
365 #[test]
367 fn domains_to_string() {
368 let domain_list = vec![Domain::One];
369 let domains = Domains::from(domain_list.as_slice());
370 assert_eq!("1", domains.to_string());
371
372 let domain_list = vec![Domain::One, Domain::Two];
373 let domains = Domains::from(domain_list.as_slice());
374 assert_eq!("1, 2", domains.to_string());
375 }
376
377 #[test]
378 fn authentication_key_try_from_path_succeeds() -> TestResult {
379 let file = {
380 let mut file = NamedTempFile::new()?;
381 let passphrase = Alphanumeric.sample_string(&mut thread_rng(), 30);
382 file.write_all(passphrase.as_bytes())?;
383 file
384 };
385
386 match AuthenticationKey::try_from(file.path()) {
387 Ok(_) => {}
388 Err(error) => panic!(
389 "Expected to create an authentication key from the contents of a file, but got an error instead: {error}"
390 ),
391 }
392
393 Ok(())
394 }
395
396 #[test]
397 fn authentication_key_try_from_path_fails_on_short_passphrase() -> TestResult {
398 let file = {
399 let mut file = NamedTempFile::new()?;
400 let passphrase = Alphanumeric.sample_string(&mut thread_rng(), 10);
401 file.write_all(passphrase.as_bytes())?;
402 file
403 };
404
405 match AuthenticationKey::try_from(file.path()) {
406 Ok(_) => panic!(
407 "Expected to fail with Error::Length, but succeeded in creating an authentication key from a passphrase file instead."
408 ),
409 Err(crate::Error::SignstarCrypto(signstar_crypto::Error::Passphrase(_))) => {}
410 Err(error) => panic!(
411 "Expected to fail with Error::Length, but failed with a different error instead: {error}"
412 ),
413 }
414
415 Ok(())
416 }
417
418 #[test]
419 fn authentication_key_try_from_path_fails_on_file_is_dir() -> TestResult {
420 let file = TempDir::new()?;
421
422 match AuthenticationKey::try_from(file.path()) {
423 Ok(_) => panic!(
424 "Expected to fail with Error::IoPath, but succeeded in creating an authentication key from a passphrase file instead."
425 ),
426 Err(crate::Error::IoPath { .. }) => {}
427 Err(error) => panic!(
428 "Expected to fail with Error::IoPath, but failed with a different error instead: {error}"
429 ),
430 }
431
432 Ok(())
433 }
434
435 #[test]
436 fn authentication_key_try_from_passphrase_succeeds() -> TestResult {
437 let passphrase = Passphrase::generate(Some(30));
438
439 match AuthenticationKey::try_from(&passphrase) {
440 Ok(_) => {}
441 Err(error) => panic!(
442 "Expected to create an authentication key from a passphrase, but got an error instead: {error}"
443 ),
444 }
445
446 Ok(())
447 }
448
449 #[test]
450 fn authentication_key_try_from_passphrase_fails_on_passphrase_too_short() -> TestResult {
451 let passphrase = Passphrase::new("passphrase".to_string());
452
453 match AuthenticationKey::try_from(&passphrase) {
454 Ok(_) => panic!("Expected to fail with Error::Length, but succeeded instead."),
455 Err(crate::Error::SignstarCrypto(signstar_crypto::Error::Passphrase(_))) => {}
456 Err(error) => panic!(
457 "Expected to fail with Error::Length, but failed with a different error instead: {error}"
458 ),
459 }
460
461 Ok(())
462 }
463}