1use std::{
4 collections::BTreeSet,
5 fmt::{Debug, Display},
6 fs::read_to_string,
7 hash::Hash,
8 path::Path,
9};
10
11use argon2::Argon2;
12use getrandom::fill;
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15#[cfg(feature = "serde")]
16use serde_repr::{Deserialize_repr, Serialize_repr};
17use signstar_crypto::passphrase::{Passphrase, PassphrasePolicy};
18use strum::{AsRefStr, IntoStaticStr};
19use yubihsm::{
20 authentication::Key as YubiHsmAuthenticationKey,
21 wrap::{Algorithm as YubiHsmWrapAlgorithm, Key as YubiHsmWrapKey},
22};
23use zeroize::Zeroizing;
24
25use crate::object::{Capabilities, Id};
26
27#[derive(
32 AsRefStr,
33 Clone,
34 Copy,
35 Debug,
36 strum::Display,
37 Eq,
38 Hash,
39 IntoStaticStr,
40 Ord,
41 PartialEq,
42 PartialOrd,
43)]
44#[cfg_attr(feature = "serde", derive(Deserialize_repr, Serialize_repr))]
45#[repr(u8)]
46pub enum Domain {
47 #[strum(serialize = "1")]
49 One = 1,
50 #[strum(serialize = "2")]
52 Two = 2,
53 #[strum(serialize = "3")]
55 Three = 3,
56 #[strum(serialize = "4")]
58 Four = 4,
59 #[strum(serialize = "5")]
61 Five = 5,
62 #[strum(serialize = "6")]
64 Six = 6,
65 #[strum(serialize = "7")]
67 Seven = 7,
68 #[strum(serialize = "8")]
70 Eight = 8,
71 #[strum(serialize = "9")]
73 Nine = 9,
74 #[strum(serialize = "10")]
76 Ten = 10,
77 #[strum(serialize = "11")]
79 Eleven = 11,
80 #[strum(serialize = "12")]
82 Twelve = 12,
83 #[strum(serialize = "13")]
85 Thirteen = 13,
86 #[strum(serialize = "14")]
88 Fourteen = 14,
89 #[strum(serialize = "15")]
91 Fifteen = 15,
92 #[strum(serialize = "16")]
94 Sixteen = 16,
95}
96
97#[cfg(feature = "cli")]
98impl clap::ValueEnum for Domain {
99 fn value_variants<'a>() -> &'a [Self] {
100 static VARIANTS: &[Domain] = &[
101 Domain::One,
102 Domain::Two,
103 Domain::Three,
104 Domain::Four,
105 Domain::Five,
106 Domain::Six,
107 Domain::Seven,
108 Domain::Eight,
109 Domain::Nine,
110 Domain::Ten,
111 Domain::Eleven,
112 Domain::Twelve,
113 Domain::Thirteen,
114 Domain::Fourteen,
115 Domain::Fifteen,
116 Domain::Sixteen,
117 ];
118 VARIANTS
119 }
120
121 fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
122 let str: &'static str = self.into();
123 Some(clap::builder::PossibleValue::new(str))
124 }
125}
126
127impl From<Domain> for yubihsm::Domain {
128 fn from(value: Domain) -> Self {
129 match value {
130 Domain::One => Self::DOM1,
131 Domain::Two => Self::DOM2,
132 Domain::Three => Self::DOM3,
133 Domain::Four => Self::DOM4,
134 Domain::Five => Self::DOM5,
135 Domain::Six => Self::DOM6,
136 Domain::Seven => Self::DOM7,
137 Domain::Eight => Self::DOM8,
138 Domain::Nine => Self::DOM9,
139 Domain::Ten => Self::DOM10,
140 Domain::Eleven => Self::DOM11,
141 Domain::Twelve => Self::DOM12,
142 Domain::Thirteen => Self::DOM13,
143 Domain::Fourteen => Self::DOM14,
144 Domain::Fifteen => Self::DOM15,
145 Domain::Sixteen => Self::DOM16,
146 }
147 }
148}
149
150#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
154#[cfg_attr(
155 feature = "serde",
156 derive(Serialize, Deserialize),
157 serde(try_from = "BTreeSet<Domain>")
158)]
159pub struct Domains(BTreeSet<Domain>);
160
161impl Domains {
162 pub fn to_be_bytes(&self) -> [u8; 2] {
164 self.bits().to_be_bytes()
165 }
166
167 pub fn all() -> Self {
169 yubihsm::Domain::all().bits().into()
170 }
171
172 pub fn bits(&self) -> u16 {
174 yubihsm::Domain::from(self).bits()
175 }
176}
177
178impl Display for Domains {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 write!(
185 f,
186 "{}",
187 self.0
188 .iter()
189 .map(|domain| domain.as_ref())
190 .collect::<Vec<_>>()
191 .join(", ")
192 )
193 }
194}
195
196impl From<Domain> for Domains {
197 fn from(value: Domain) -> Self {
198 Self(BTreeSet::from_iter([value]))
199 }
200}
201
202impl From<yubihsm::Domain> for Domains {
203 fn from(value: yubihsm::Domain) -> Self {
204 let lookup = [
205 (yubihsm::Domain::DOM1, Domain::One),
206 (yubihsm::Domain::DOM2, Domain::Two),
207 (yubihsm::Domain::DOM3, Domain::Three),
208 (yubihsm::Domain::DOM4, Domain::Four),
209 (yubihsm::Domain::DOM5, Domain::Five),
210 (yubihsm::Domain::DOM6, Domain::Six),
211 (yubihsm::Domain::DOM7, Domain::Seven),
212 (yubihsm::Domain::DOM8, Domain::Eight),
213 (yubihsm::Domain::DOM9, Domain::Nine),
214 (yubihsm::Domain::DOM10, Domain::Ten),
215 (yubihsm::Domain::DOM11, Domain::Eleven),
216 (yubihsm::Domain::DOM12, Domain::Twelve),
217 (yubihsm::Domain::DOM13, Domain::Thirteen),
218 (yubihsm::Domain::DOM14, Domain::Fourteen),
219 (yubihsm::Domain::DOM15, Domain::Fifteen),
220 (yubihsm::Domain::DOM16, Domain::Sixteen),
221 ];
222
223 Domains(BTreeSet::from_iter(lookup.iter().filter_map(
224 |(yubi_dom, dom)| {
225 if value.contains(*yubi_dom) {
226 Some(*dom)
227 } else {
228 None
229 }
230 },
231 )))
232 }
233}
234
235impl From<u16> for Domains {
236 fn from(value: u16) -> Self {
237 yubihsm::Domain::from_bits_retain(value).into()
238 }
239}
240
241impl From<&[Domain]> for Domains {
242 fn from(value: &[Domain]) -> Self {
243 Self(value.iter().copied().collect())
244 }
245}
246
247impl TryFrom<BTreeSet<Domain>> for Domains {
248 type Error = crate::object::Error;
249
250 fn try_from(domains: BTreeSet<Domain>) -> Result<Self, Self::Error> {
251 if domains.is_empty() {
252 return Err(Self::Error::EmptySetOfDomains);
253 }
254 Ok(Self(domains))
255 }
256}
257
258impl From<&Domains> for yubihsm::Domain {
259 fn from(value: &Domains) -> Self {
260 value
261 .0
262 .iter()
263 .map(|cap| yubihsm::Domain::from(*cap))
264 .fold(yubihsm::Domain::empty(), |acc, c| acc | c)
265 }
266}
267
268#[derive(Debug)]
270pub struct AuthenticationKey(YubiHsmAuthenticationKey);
271
272impl AuthenticationKey {
273 pub const PASSPHRASE_POLICY: PassphrasePolicy = PassphrasePolicy { minimum_length: 30 };
275}
276
277impl AsRef<YubiHsmAuthenticationKey> for AuthenticationKey {
278 fn as_ref(&self) -> &YubiHsmAuthenticationKey {
279 &self.0
280 }
281}
282
283impl From<AuthenticationKey> for YubiHsmAuthenticationKey {
284 fn from(value: AuthenticationKey) -> Self {
285 value.0
286 }
287}
288
289impl From<&AuthenticationKey> for YubiHsmAuthenticationKey {
290 fn from(value: &AuthenticationKey) -> Self {
291 value.0.clone()
292 }
293}
294
295impl TryFrom<&Path> for AuthenticationKey {
296 type Error = crate::Error;
297
298 fn try_from(file: &Path) -> Result<Self, Self::Error> {
310 let passphrase = Passphrase::new_with_policy(
311 read_to_string(file).map_err(|source| crate::Error::IoPath {
312 path: file.into(),
313 context: "reading the passphrase for an authentication key derivation from file",
314 source,
315 })?,
316 &Self::PASSPHRASE_POLICY,
317 )?;
318
319 Ok(Self(YubiHsmAuthenticationKey::derive_from_password(
320 passphrase.expose_borrowed().as_bytes(),
321 )))
322 }
323}
324
325impl TryFrom<&Passphrase> for AuthenticationKey {
326 type Error = crate::Error;
327
328 fn try_from(passphrase: &Passphrase) -> Result<Self, Self::Error> {
336 passphrase.check_against_policy(&Self::PASSPHRASE_POLICY)?;
337
338 Ok(Self(YubiHsmAuthenticationKey::derive_from_password(
339 passphrase.expose_borrowed().as_bytes(),
340 )))
341 }
342}
343
344#[derive(Clone, Copy, Debug, Default, Eq, IntoStaticStr, PartialEq)]
346pub enum WrapKeyKind {
347 Aes128,
349
350 Aes192,
352
353 #[default]
361 Aes256,
362}
363
364impl WrapKeyKind {
365 pub fn key_len(&self) -> usize {
367 match self {
368 Self::Aes128 => 16,
369 Self::Aes192 => 24,
370 Self::Aes256 => 32,
371 }
372 }
373}
374
375impl From<&WrapKeyKind> for YubiHsmWrapAlgorithm {
376 fn from(value: &WrapKeyKind) -> Self {
377 match value {
378 WrapKeyKind::Aes128 => Self::Aes128Ccm,
379 WrapKeyKind::Aes192 => Self::Aes192Ccm,
380 WrapKeyKind::Aes256 => Self::Aes256Ccm,
381 }
382 }
383}
384
385pub struct WrapKey {
389 kind: WrapKeyKind,
390 data: Zeroizing<Vec<u8>>,
391}
392
393impl WrapKey {
394 pub const PASSPHRASE_POLICY: PassphrasePolicy = PassphrasePolicy {
396 minimum_length: 100,
397 };
398
399 pub fn generate_random(kind: WrapKeyKind) -> Result<Self, crate::Error> {
405 let data = {
406 let mut bytes = Zeroizing::new(vec![0u8; kind.key_len()]);
407 fill(&mut bytes).map_err(|source| crate::object::Error::GetRandom {
408 context: "generating a random wrapping key",
409 source,
410 })?;
411 bytes
412 };
413
414 Ok(Self { kind, data })
415 }
416}
417
418impl Debug for WrapKey {
419 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420 f.debug_struct("WrapKey")
421 .field("kind", &self.kind)
422 .field("data", &"[REDACTED]")
423 .finish()
424 }
425}
426
427impl From<&WrapKey> for Vec<u8> {
428 fn from(value: &WrapKey) -> Self {
429 value.data.to_vec()
430 }
431}
432
433#[derive(Debug)]
440pub struct WrapKeyFromPassphrase<'passphrase> {
441 passphrase: &'passphrase Passphrase,
442 kind: WrapKeyKind,
443}
444
445impl<'passphrase> WrapKeyFromPassphrase<'passphrase> {
446 pub(crate) const ARGON2_SALT: &'static [u8] = b"Salt for a Signstar backup key";
448
449 pub fn new(
462 passphrase: &'passphrase Passphrase,
463 kind: WrapKeyKind,
464 ) -> Result<Self, crate::Error> {
465 passphrase.check_against_policy(&WrapKey::PASSPHRASE_POLICY)?;
466
467 Ok(Self { passphrase, kind })
468 }
469}
470
471impl<'passphrase> TryFrom<WrapKeyFromPassphrase<'passphrase>> for WrapKey {
472 type Error = crate::Error;
473
474 fn try_from(value: WrapKeyFromPassphrase<'passphrase>) -> Result<Self, Self::Error> {
486 let mut data = Zeroizing::new(vec![0u8; value.kind.key_len()]);
487 Argon2::default()
488 .hash_password_into(
489 value.passphrase.expose_borrowed().as_bytes(),
490 WrapKeyFromPassphrase::ARGON2_SALT,
491 &mut data,
492 )
493 .map_err(|source| crate::object::Error::Argon2 {
494 context: "creating a wrap key from a passphrase",
495 source,
496 })?;
497
498 Ok(WrapKey {
499 kind: value.kind,
500 data,
501 })
502 }
503}
504
505#[derive(Debug)]
509pub struct YubiHsmWrapKeyFromWrapKey<'wrap_key> {
510 pub(crate) id: Id,
511 pub(crate) wrap_key: &'wrap_key WrapKey,
512}
513
514impl<'wrap_key> TryFrom<&YubiHsmWrapKeyFromWrapKey<'wrap_key>> for YubiHsmWrapKey {
515 type Error = crate::Error;
516
517 fn try_from(value: &YubiHsmWrapKeyFromWrapKey) -> Result<Self, Self::Error> {
523 Self::from_bytes(value.id.into(), &value.wrap_key.data).map_err(|source| {
524 crate::Error::Device {
525 context: "creating a YubiHSM2 wrap key from bytes",
526 source,
527 }
528 })
529 }
530}
531
532#[derive(Clone, Debug)]
537#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
538pub struct KeyInfo {
539 pub key_id: Id,
541
542 pub domains: Domains,
547
548 pub caps: Capabilities,
550}
551
552#[cfg(test)]
553mod tests {
554 use std::io::Write;
555
556 use rand::{
557 distributions::{Alphanumeric, DistString},
558 thread_rng,
559 };
560 use rstest::{fixture, rstest};
561 use tempfile::{NamedTempFile, TempDir};
562 use testresult::TestResult;
563
564 use super::*;
565
566 #[test]
568 fn domains_to_string() {
569 let domain_list = vec![Domain::One];
570 let domains = Domains::from(domain_list.as_slice());
571 assert_eq!("1", domains.to_string());
572
573 let domain_list = vec![Domain::One, Domain::Two];
574 let domains = Domains::from(domain_list.as_slice());
575 assert_eq!("1, 2", domains.to_string());
576 }
577
578 #[test]
579 fn authentication_key_try_from_path_succeeds() -> TestResult {
580 let file = {
581 let mut file = NamedTempFile::new()?;
582 let passphrase = Alphanumeric.sample_string(&mut thread_rng(), 30);
583 file.write_all(passphrase.as_bytes())?;
584 file
585 };
586
587 match AuthenticationKey::try_from(file.path()) {
588 Ok(_) => {}
589 Err(error) => panic!(
590 "Expected to create an authentication key from the contents of a file, but got an error instead: {error}"
591 ),
592 }
593
594 Ok(())
595 }
596
597 #[test]
598 fn authentication_key_try_from_path_fails_on_short_passphrase() -> TestResult {
599 let file = {
600 let mut file = NamedTempFile::new()?;
601 let passphrase = Alphanumeric.sample_string(&mut thread_rng(), 10);
602 file.write_all(passphrase.as_bytes())?;
603 file
604 };
605
606 match AuthenticationKey::try_from(file.path()) {
607 Ok(_) => panic!(
608 "Expected to fail with Error::Length, but succeeded in creating an authentication key from a passphrase file instead."
609 ),
610 Err(crate::Error::SignstarCrypto(signstar_crypto::Error::Passphrase(_))) => {}
611 Err(error) => panic!(
612 "Expected to fail with Error::Length, but failed with a different error instead: {error}"
613 ),
614 }
615
616 Ok(())
617 }
618
619 #[test]
620 fn authentication_key_try_from_path_fails_on_file_is_dir() -> TestResult {
621 let file = TempDir::new()?;
622
623 match AuthenticationKey::try_from(file.path()) {
624 Ok(_) => panic!(
625 "Expected to fail with Error::IoPath, but succeeded in creating an authentication key from a passphrase file instead."
626 ),
627 Err(crate::Error::IoPath { .. }) => {}
628 Err(error) => panic!(
629 "Expected to fail with Error::IoPath, but failed with a different error instead: {error}"
630 ),
631 }
632
633 Ok(())
634 }
635
636 #[test]
637 fn authentication_key_try_from_passphrase_succeeds() -> TestResult {
638 let passphrase = Passphrase::generate(Some(30));
639
640 match AuthenticationKey::try_from(&passphrase) {
641 Ok(_) => {}
642 Err(error) => panic!(
643 "Expected to create an authentication key from a passphrase, but got an error instead: {error}"
644 ),
645 }
646
647 Ok(())
648 }
649
650 #[test]
651 fn authentication_key_try_from_passphrase_fails_on_passphrase_too_short() -> TestResult {
652 let passphrase = Passphrase::new("passphrase".to_string());
653
654 match AuthenticationKey::try_from(&passphrase) {
655 Ok(_) => panic!("Expected to fail with Error::Length, but succeeded instead."),
656 Err(crate::Error::SignstarCrypto(signstar_crypto::Error::Passphrase(_))) => {}
657 Err(error) => panic!(
658 "Expected to fail with Error::Length, but failed with a different error instead: {error}"
659 ),
660 }
661
662 Ok(())
663 }
664
665 #[rstest]
667 #[case(WrapKeyKind::Aes128, 16)]
668 #[case(WrapKeyKind::Aes192, 24)]
669 #[case(WrapKeyKind::Aes256, 32)]
670 fn wrap_key_kind_key_len(#[case] wrap_key_kind: WrapKeyKind, #[case] len: usize) {
671 assert_eq!(wrap_key_kind.key_len(), len);
672 }
673
674 #[rstest]
677 #[case(WrapKeyKind::Aes128, YubiHsmWrapAlgorithm::Aes128Ccm)]
678 #[case(WrapKeyKind::Aes192, YubiHsmWrapAlgorithm::Aes192Ccm)]
679 #[case(WrapKeyKind::Aes256, YubiHsmWrapAlgorithm::Aes256Ccm)]
680 fn yubihsm_wrap_algorithm_from_wrap_key_kind(
681 #[case] wrap_key_kind: WrapKeyKind,
682 #[case] algorithm: YubiHsmWrapAlgorithm,
683 ) {
684 assert_eq!(YubiHsmWrapAlgorithm::from(&wrap_key_kind), algorithm);
685 }
686
687 #[rstest]
689 #[case(WrapKeyKind::Aes128)]
690 #[case(WrapKeyKind::Aes192)]
691 #[case(WrapKeyKind::Aes256)]
692 fn wrap_key_generate_random_succeeds(#[case] wrap_key_kind: WrapKeyKind) -> TestResult {
693 let wrap_key = WrapKey::generate_random(wrap_key_kind)?;
694 let data: Vec<u8> = From::from(&wrap_key);
695
696 assert_eq!(data.len(), wrap_key_kind.key_len());
697
698 Ok(())
699 }
700
701 #[rstest]
703 #[case(WrapKeyKind::Aes128)]
704 #[case(WrapKeyKind::Aes192)]
705 #[case(WrapKeyKind::Aes256)]
706 fn wrap_key_debug(#[case] wrap_key_kind: WrapKeyKind) -> TestResult {
707 let wrap_key = WrapKey::generate_random(wrap_key_kind)?;
708 let data_debug = format!("{:?}", wrap_key.data.to_vec());
709 let wrap_key_debug = format!("{wrap_key:?}");
710 let wrap_key_kind_debug = format!("{wrap_key_kind:?}");
711
712 assert!(wrap_key_debug.contains(&wrap_key_kind_debug));
713 assert!(wrap_key_debug.contains("[REDACTED]"));
714 assert!(!wrap_key_debug.contains(&data_debug));
715
716 Ok(())
717 }
718
719 #[fixture]
721 fn valid_wrap_key_passphrase() -> Passphrase {
722 Passphrase::new("this is a long passphrase that is at least 100 chars long, very long omg, so long, really now, you gotta believe me".to_string())
723 }
724
725 #[fixture]
727 fn invalid_wrap_key_passphrase() -> Passphrase {
728 Passphrase::new("this passphrase is shorter than 100 chars".to_string())
729 }
730
731 #[rstest]
733 #[case(WrapKeyKind::Aes128)]
734 #[case(WrapKeyKind::Aes192)]
735 #[case(WrapKeyKind::Aes256)]
736 fn wrap_key_from_passphrase_new_succeeds(
737 #[case] wrap_key_kind: WrapKeyKind,
738 valid_wrap_key_passphrase: Passphrase,
739 ) -> TestResult {
740 WrapKeyFromPassphrase::new(&valid_wrap_key_passphrase, wrap_key_kind)?;
741
742 Ok(())
743 }
744
745 #[rstest]
747 #[case(WrapKeyKind::Aes128)]
748 #[case(WrapKeyKind::Aes192)]
749 #[case(WrapKeyKind::Aes256)]
750 fn wrap_key_from_passphrase_new_fails_on_short_passphrase(
751 #[case] wrap_key_kind: WrapKeyKind,
752 invalid_wrap_key_passphrase: Passphrase,
753 ) -> TestResult {
754 assert!(WrapKeyFromPassphrase::new(&invalid_wrap_key_passphrase, wrap_key_kind).is_err());
755
756 Ok(())
757 }
758
759 #[rstest]
761 #[case(WrapKeyKind::Aes128)]
762 #[case(WrapKeyKind::Aes192)]
763 #[case(WrapKeyKind::Aes256)]
764 fn wrap_key_try_from_wrap_key_from_passphrase_succeeds(
765 #[case] wrap_key_kind: WrapKeyKind,
766 valid_wrap_key_passphrase: Passphrase,
767 ) -> TestResult {
768 let wrap_key_from_passphrase =
769 WrapKeyFromPassphrase::new(&valid_wrap_key_passphrase, wrap_key_kind)?;
770 let wrap_key = WrapKey::try_from(wrap_key_from_passphrase)?;
771 let data: Vec<u8> = From::from(&wrap_key);
772
773 assert_eq!(data.len(), wrap_key_kind.key_len());
774
775 Ok(())
776 }
777
778 #[rstest]
780 #[case(WrapKeyKind::Aes128)]
781 #[case(WrapKeyKind::Aes192)]
782 #[case(WrapKeyKind::Aes256)]
783 fn yubihsm_wrap_key_try_from_yubihsm_wrap_key_from_wrap_key_succeeds(
784 #[case] wrap_key_kind: WrapKeyKind,
785 valid_wrap_key_passphrase: Passphrase,
786 ) -> TestResult {
787 let wrap_key = {
788 let wrap_key_from_passphrase =
789 WrapKeyFromPassphrase::new(&valid_wrap_key_passphrase, wrap_key_kind)?;
790 WrapKey::try_from(wrap_key_from_passphrase)?
791 };
792 let yubihsm_wrap_key_from_wrap_key = YubiHsmWrapKeyFromWrapKey {
793 id: "1".parse()?,
794 wrap_key: &wrap_key,
795 };
796
797 YubiHsmWrapKey::try_from(&yubihsm_wrap_key_from_wrap_key)?;
798
799 Ok(())
800 }
801}