rite_sdk/types.rs
1//! Data types shared between the runtime and backend implementations.
2//!
3//! These types form the vocabulary of the backend interface: key specifications,
4//! algorithm identifiers, attestation evidence, and hardware-specific metadata.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9use crate::backend::BackendError;
10use crate::key_material::PublicKeyDer;
11
12/// Opaque key identifier (backend-specific).
13///
14/// This is an opaque reference to a key managed by a backend. The internal
15/// format is backend-specific (e.g., `"slot_9c"` for `YubiKey` PIV, UUID for
16/// software backend).
17///
18/// Ceremony DSL never interprets the contents of a `KeyId`; it is purely a
19/// reference that gets passed back to the backend.
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(transparent)]
22pub struct KeyId(String);
23
24impl KeyId {
25 /// Create a new `KeyId` from a string.
26 pub fn new(s: impl Into<String>) -> Self {
27 Self(s.into())
28 }
29
30 /// Return the `KeyId` as a string slice.
31 pub fn as_str(&self) -> &str {
32 &self.0
33 }
34}
35
36impl AsRef<str> for KeyId {
37 fn as_ref(&self) -> &str {
38 &self.0
39 }
40}
41
42impl fmt::Display for KeyId {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 write!(f, "{}", self.0)
45 }
46}
47
48impl From<String> for KeyId {
49 fn from(s: String) -> Self {
50 Self(s)
51 }
52}
53
54impl From<&str> for KeyId {
55 fn from(s: &str) -> Self {
56 Self(s.to_string())
57 }
58}
59
60/// Metadata returned after key generation or import.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct KeyMetadata {
63 /// Opaque backend-specific key identifier.
64 pub key_id: KeyId,
65 /// Key algorithm.
66 pub algorithm: KeyAlgorithm,
67 /// Human-readable label.
68 pub label: String,
69 /// Public key, if the backend exports one.
70 pub public_key: Option<PublicKeyDer>,
71 /// Key check value, for a symmetric key.
72 ///
73 /// The counterpart of `public_key`: what the key answers to in the
74 /// transcript and in the room. `None` for an asymmetric key, which is
75 /// named by its public half.
76 pub check_value: Option<KeyCheckValue>,
77 /// Attestation evidence (if backend supports attestation).
78 pub attestation: Option<Attestation>,
79}
80
81/// What a symmetric key answers to, in place of a public key.
82///
83/// AES-CMAC over a 16-byte zero block under the key, leftmost three bytes, as
84/// specified for AES keys in ANSI X9.24-1 Annex A and used by TR-31. It is
85/// what an HSM displays and what custodians read back, so a transcript
86/// carrying it records the value the room checked rather than a parallel one
87/// only Rite can compute.
88///
89/// Three bytes, not a digest of the key. A full-length hash of a secret is
90/// what rite#126 removed from the transcript, and a reader cannot tell by
91/// looking whether such a value came from a 256-bit key or a 4-digit PIN. The
92/// truncation here is the part that makes it evident.
93///
94/// Renders as `cmac-aes:763cbc`. The method is in the string because the
95/// older convention of encrypting a zero block in ECB is still in use and
96/// gives a different answer for the same key.
97///
98/// ```
99/// use rite_sdk::KeyCheckValue;
100///
101/// // A backend computes the CMAC; this keeps the leftmost bytes of it.
102/// let cmac = [0x76, 0x3c, 0xbc, 0xde, 0x81, 0xdf, 0x91, 0x31];
103/// let kcv = KeyCheckValue::from_cmac(&cmac)?;
104///
105/// assert_eq!(kcv.to_string(), "cmac-aes:763cbc");
106/// assert_eq!(kcv.as_bytes(), &[0x76, 0x3c, 0xbc]);
107///
108/// // It round-trips through the string a transcript records.
109/// assert_eq!("cmac-aes:763cbc".parse::<KeyCheckValue>()?, kcv);
110/// // Uppercase hex is a different string for the same bytes, so it is not one.
111/// assert!("cmac-aes:763CBC".parse::<KeyCheckValue>().is_err());
112/// # Ok::<(), rite_sdk::ParseError>(())
113/// ```
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(into = "String", try_from = "String")]
116pub struct KeyCheckValue([u8; Self::LEN]);
117
118impl KeyCheckValue {
119 /// Bytes kept from the CMAC output.
120 pub const LEN: usize = 3;
121
122 /// The name this construction goes by, and the string's prefix.
123 pub const METHOD: &'static str = "cmac-aes";
124
125 /// Keep the leftmost bytes of a CMAC output.
126 ///
127 /// # Errors
128 ///
129 /// If `cmac` is shorter than [`Self::LEN`], which no AES-CMAC is.
130 pub fn from_cmac(cmac: &[u8]) -> Result<Self, ParseError> {
131 cmac.get(..Self::LEN)
132 .and_then(|head| head.try_into().ok())
133 .map(Self)
134 .ok_or_else(|| ParseError(format!("a CMAC of {} bytes", cmac.len())))
135 }
136
137 /// The kept bytes.
138 #[must_use]
139 pub fn as_bytes(&self) -> &[u8; Self::LEN] {
140 &self.0
141 }
142}
143
144impl fmt::Display for KeyCheckValue {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 write!(f, "{}:", Self::METHOD)?;
147 for byte in self.0 {
148 write!(f, "{byte:02x}")?;
149 }
150 Ok(())
151 }
152}
153
154impl std::str::FromStr for KeyCheckValue {
155 type Err = ParseError;
156
157 fn from_str(s: &str) -> Result<Self, Self::Err> {
158 let hex = s
159 .strip_prefix(Self::METHOD)
160 .and_then(|rest| rest.strip_prefix(':'))
161 // Lowercase only, so the string a `KeyCheckValue` parses from is
162 // the string it renders back to and two records of one key
163 // compare as text.
164 .filter(|hex| {
165 hex.len() == 2 * Self::LEN
166 && hex.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
167 })
168 .ok_or_else(|| ParseError(s.to_owned()))?;
169 let (pairs, _) = hex.as_bytes().as_chunks::<2>();
170 let mut bytes = [0u8; Self::LEN];
171 for (byte, pair) in bytes.iter_mut().zip(pairs) {
172 let pair = std::str::from_utf8(pair).map_err(|_| ParseError(s.to_owned()))?;
173 *byte = u8::from_str_radix(pair, 16).map_err(|_| ParseError(s.to_owned()))?;
174 }
175 Ok(Self(bytes))
176 }
177}
178
179impl From<KeyCheckValue> for String {
180 fn from(kcv: KeyCheckValue) -> String {
181 kcv.to_string()
182 }
183}
184
185impl TryFrom<String> for KeyCheckValue {
186 type Error = ParseError;
187
188 fn try_from(s: String) -> Result<Self, Self::Error> {
189 s.parse()
190 }
191}
192
193/// Key algorithm.
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
195#[serde(into = "String", try_from = "String")]
196#[non_exhaustive]
197pub enum KeyAlgorithm {
198 /// RSA 2048-bit key.
199 Rsa2048,
200 /// RSA 4096-bit key.
201 Rsa4096,
202 /// ECDSA with P-256 curve (secp256r1).
203 EcdsaP256,
204 /// ECDSA with P-384 curve (secp384r1).
205 EcdsaP384,
206 /// Ed25519 (`EdDSA`).
207 Ed25519,
208 /// ML-DSA-44, module-lattice signature at NIST security category 2 (FIPS 204).
209 MlDsa44,
210 /// ML-DSA-65, module-lattice signature at NIST security category 3 (FIPS 204).
211 MlDsa65,
212 /// ML-DSA-87, module-lattice signature at NIST security category 5 (FIPS 204).
213 MlDsa87,
214 /// ML-KEM-512, module-lattice KEM at NIST security category 1 (FIPS 203).
215 ///
216 /// A KEM encapsulates, it does not sign. Such a key can receive a wrap and
217 /// nothing else.
218 MlKem512,
219 /// ML-KEM-768, module-lattice KEM at NIST security category 3 (FIPS 203).
220 MlKem768,
221 /// ML-KEM-1024, module-lattice KEM at NIST security category 5 (FIPS 203).
222 MlKem1024,
223 /// AES 128-bit symmetric key.
224 Aes128,
225 /// AES 256-bit symmetric key.
226 Aes256,
227}
228
229impl KeyAlgorithm {
230 /// The signature algorithm to use with this key unless told otherwise.
231 ///
232 /// `None` for symmetric keys, which sign nothing.
233 ///
234 /// A key algorithm does not always determine a signature algorithm: RSA
235 /// keys work with both PKCS#1 v1.5 and PSS, and this picks v1.5 for
236 /// interoperability. Everywhere else the pairing is forced, either by the
237 /// curve's matching digest strength (RFC 5480) or by the scheme naming its
238 /// own digest (Ed25519, ML-DSA).
239 #[must_use]
240 pub fn default_sign_algorithm(self) -> Option<SignAlgorithm> {
241 match self {
242 KeyAlgorithm::Rsa2048 | KeyAlgorithm::Rsa4096 => Some(SignAlgorithm::RsaPkcs1Sha256),
243 KeyAlgorithm::EcdsaP256 => Some(SignAlgorithm::EcdsaSha256),
244 KeyAlgorithm::EcdsaP384 => Some(SignAlgorithm::EcdsaSha384),
245 KeyAlgorithm::Ed25519 => Some(SignAlgorithm::Ed25519),
246 KeyAlgorithm::MlDsa44 => Some(SignAlgorithm::MlDsa44),
247 KeyAlgorithm::MlDsa65 => Some(SignAlgorithm::MlDsa65),
248 KeyAlgorithm::MlDsa87 => Some(SignAlgorithm::MlDsa87),
249 KeyAlgorithm::MlKem512
250 | KeyAlgorithm::MlKem768
251 | KeyAlgorithm::MlKem1024
252 | KeyAlgorithm::Aes128
253 | KeyAlgorithm::Aes256 => None,
254 }
255 }
256
257 /// Whether a key of this algorithm can be the recipient of a wrap.
258 ///
259 /// A recipient has to be able to receive a content-encryption key, by key
260 /// transport, key agreement, or encapsulation. A signature algorithm can
261 /// do none of the three, however good a key it otherwise is, and finding
262 /// that out mid-ceremony is how it used to surface.
263 #[must_use]
264 pub fn can_receive_wrap(self) -> bool {
265 match self {
266 KeyAlgorithm::Rsa2048
267 | KeyAlgorithm::Rsa4096
268 | KeyAlgorithm::EcdsaP256
269 | KeyAlgorithm::EcdsaP384
270 | KeyAlgorithm::MlKem512
271 | KeyAlgorithm::MlKem768
272 | KeyAlgorithm::MlKem1024 => true,
273
274 // Ed25519 and ML-DSA sign and nothing else. A symmetric key is a
275 // pre-shared KEK, which is a different recipient form than the
276 // ones Rite produces.
277 KeyAlgorithm::Ed25519
278 | KeyAlgorithm::MlDsa44
279 | KeyAlgorithm::MlDsa65
280 | KeyAlgorithm::MlDsa87
281 | KeyAlgorithm::Aes128
282 | KeyAlgorithm::Aes256 => false,
283 }
284 }
285
286 /// Whether a key of this algorithm is one secret with no public half.
287 ///
288 /// ```
289 /// use rite_sdk::KeyAlgorithm;
290 ///
291 /// assert!(KeyAlgorithm::Aes256.is_symmetric());
292 /// assert_eq!(KeyAlgorithm::Aes256.key_bytes(), Some(32));
293 ///
294 /// assert!(!KeyAlgorithm::EcdsaP256.is_symmetric());
295 /// assert_eq!(KeyAlgorithm::EcdsaP256.key_bytes(), None);
296 /// ```
297 ///
298 /// This is the discriminator for everything that assumes a keypair: there
299 /// is no public key to export, no SPKI to fingerprint, and no certificate
300 /// to issue. A backend answers for such a key with a
301 /// [`KeyCheckValue`](crate::KeyCheckValue) instead.
302 #[must_use]
303 pub fn is_symmetric(self) -> bool {
304 self.key_bytes().is_some()
305 }
306
307 /// The length of the secret, for an algorithm whose key is raw bytes.
308 ///
309 /// `None` for an asymmetric algorithm, whose key has no single length a
310 /// caller could ask for.
311 #[must_use]
312 pub fn key_bytes(self) -> Option<usize> {
313 match self {
314 KeyAlgorithm::Aes128 => Some(16),
315 KeyAlgorithm::Aes256 => Some(32),
316 KeyAlgorithm::Rsa2048
317 | KeyAlgorithm::Rsa4096
318 | KeyAlgorithm::EcdsaP256
319 | KeyAlgorithm::EcdsaP384
320 | KeyAlgorithm::Ed25519
321 | KeyAlgorithm::MlDsa44
322 | KeyAlgorithm::MlDsa65
323 | KeyAlgorithm::MlDsa87
324 | KeyAlgorithm::MlKem512
325 | KeyAlgorithm::MlKem768
326 | KeyAlgorithm::MlKem1024 => None,
327 }
328 }
329}
330
331impl fmt::Display for KeyAlgorithm {
332 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333 match self {
334 KeyAlgorithm::Rsa2048 => write!(f, "RSA-2048"),
335 KeyAlgorithm::Rsa4096 => write!(f, "RSA-4096"),
336 KeyAlgorithm::EcdsaP256 => write!(f, "ECDSA-P256"),
337 KeyAlgorithm::EcdsaP384 => write!(f, "ECDSA-P384"),
338 KeyAlgorithm::Ed25519 => write!(f, "Ed25519"),
339 KeyAlgorithm::MlDsa44 => write!(f, "ML-DSA-44"),
340 KeyAlgorithm::MlDsa65 => write!(f, "ML-DSA-65"),
341 KeyAlgorithm::MlDsa87 => write!(f, "ML-DSA-87"),
342 KeyAlgorithm::MlKem512 => write!(f, "ML-KEM-512"),
343 KeyAlgorithm::MlKem768 => write!(f, "ML-KEM-768"),
344 KeyAlgorithm::MlKem1024 => write!(f, "ML-KEM-1024"),
345 KeyAlgorithm::Aes128 => write!(f, "AES-128"),
346 KeyAlgorithm::Aes256 => write!(f, "AES-256"),
347 }
348 }
349}
350
351/// Error returned when parsing an SDK algorithm identifier from a string fails.
352///
353/// Shared across all algorithm enums in this crate.
354#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
355#[error("unknown value: {0:?}")]
356pub struct ParseError(String);
357
358impl std::str::FromStr for KeyAlgorithm {
359 type Err = ParseError;
360
361 fn from_str(s: &str) -> Result<Self, Self::Err> {
362 match s {
363 "RSA-2048" => Ok(Self::Rsa2048),
364 "RSA-4096" => Ok(Self::Rsa4096),
365 "ECDSA-P256" => Ok(Self::EcdsaP256),
366 "ECDSA-P384" => Ok(Self::EcdsaP384),
367 "Ed25519" => Ok(Self::Ed25519),
368 "ML-DSA-44" => Ok(Self::MlDsa44),
369 "ML-DSA-65" => Ok(Self::MlDsa65),
370 "ML-DSA-87" => Ok(Self::MlDsa87),
371 "ML-KEM-512" => Ok(Self::MlKem512),
372 "ML-KEM-768" => Ok(Self::MlKem768),
373 "ML-KEM-1024" => Ok(Self::MlKem1024),
374 "AES-128" => Ok(Self::Aes128),
375 "AES-256" => Ok(Self::Aes256),
376 _ => Err(ParseError(s.to_owned())),
377 }
378 }
379}
380
381impl From<KeyAlgorithm> for String {
382 fn from(a: KeyAlgorithm) -> String {
383 a.to_string()
384 }
385}
386
387impl TryFrom<String> for KeyAlgorithm {
388 type Error = ParseError;
389
390 fn try_from(s: String) -> Result<Self, Self::Error> {
391 s.parse()
392 }
393}
394
395bitflags::bitflags! {
396 /// Key usage flags. Maps to PKCS#11 `CKA_SIGN`, `CKA_VERIFY`, etc.
397 ///
398 /// PIV ignores these (slot determines usage). PKCS#11 requires them at creation.
399 /// Using bitflags rather than 7 bools: more compact for serialization, makes
400 /// set operations natural, and avoids nonsensical bool combinations.
401 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
402 pub struct KeyUsages: u8 {
403 /// Key may be used to sign data.
404 const SIGN = 0b0000_0001;
405 /// Key may be used to verify signatures.
406 const VERIFY = 0b0000_0010;
407 /// Key may be used to encrypt data.
408 const ENCRYPT = 0b0000_0100;
409 /// Key may be used to decrypt data.
410 const DECRYPT = 0b0000_1000;
411 /// Key may be used to wrap other keys.
412 const WRAP = 0b0001_0000;
413 /// Key may be used to unwrap other keys.
414 const UNWRAP = 0b0010_0000;
415 /// Key may be used to derive other keys.
416 const DERIVE = 0b0100_0000;
417 }
418}
419
420impl KeyUsages {
421 /// Every usage, paired with the name a ceremony writes for it.
422 ///
423 /// The names are PKCS#11 vocabulary. They say what a key is permitted to do
424 /// at the token, which is a different question from the `KeyUsage`
425 /// extension a certificate carries.
426 pub const NAMED: [(&'static str, KeyUsages); 7] = [
427 ("sign", KeyUsages::SIGN),
428 ("verify", KeyUsages::VERIFY),
429 ("encrypt", KeyUsages::ENCRYPT),
430 ("decrypt", KeyUsages::DECRYPT),
431 ("wrap", KeyUsages::WRAP),
432 ("unwrap", KeyUsages::UNWRAP),
433 ("derive", KeyUsages::DERIVE),
434 ];
435
436 /// Look up a single usage by its ceremony name.
437 ///
438 /// Named to avoid colliding with the bitflags-generated `from_name`,
439 /// which matches on the `SCREAMING_CASE` constant names instead.
440 #[must_use]
441 pub fn usage_named(name: &str) -> Option<Self> {
442 Self::NAMED
443 .iter()
444 .find(|(candidate, _)| *candidate == name)
445 .map(|(_, usage)| *usage)
446 }
447
448 /// The names of the usages in this set, in declaration order.
449 #[must_use]
450 pub fn names(self) -> Vec<&'static str> {
451 Self::NAMED
452 .iter()
453 .filter(|(_, usage)| self.contains(*usage))
454 .map(|(name, _)| *name)
455 .collect()
456 }
457
458 /// Every usage name a ceremony may write, for a message listing what is
459 /// available.
460 #[must_use]
461 pub fn all_names() -> Vec<&'static str> {
462 Self::NAMED.iter().map(|(name, _)| *name).collect()
463 }
464
465 /// The usages in this set as one phrase, for an error message.
466 #[must_use]
467 pub fn describe(self) -> String {
468 let names = self.names();
469 if names.is_empty() {
470 "nothing".to_string()
471 } else {
472 names.join(", ")
473 }
474 }
475}
476
477/// Security and usage policy for a generated or imported key.
478///
479/// Backends that cannot honour a requested policy MUST return
480/// `BackendError::OperationNotPermitted` rather than silently ignoring it.
481/// For example, a PIV backend receiving `extractable: true` must reject it
482/// because PIV keys are always non-extractable by hardware design.
483// Each bool maps directly to a named PKCS#11 boolean attribute (CKA_TOKEN,
484// CKA_SENSITIVE, CKA_EXTRACTABLE, CKA_WRAP_WITH_TRUSTED). Converting these
485// to two-variant enums would add type noise with no semantic benefit.
486#[allow(clippy::struct_excessive_bools)]
487#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct KeyPolicy {
489 /// Key persists after session ends. Always `true` for PIV. `CKA_TOKEN` in PKCS#11.
490 pub persistent: bool,
491 /// Key material never revealed in plaintext. Always `true` for PIV. `CKA_SENSITIVE`.
492 pub sensitive: bool,
493 /// Key can be wrapped and exported. Always `false` for PIV. `CKA_EXTRACTABLE`.
494 pub extractable: bool,
495 /// Key can only be wrapped by a `CKA_TRUSTED` wrapping key. PKCS#11 `CKA_WRAP_WITH_TRUSTED`.
496 pub wrap_with_trusted_only: bool,
497 /// What operations this key is permitted to perform.
498 pub usages: KeyUsages,
499}
500
501impl KeyPolicy {
502 /// Refuse an operation this policy does not allow.
503 ///
504 /// The policy is what the ceremony asked for at generation. A token
505 /// enforces it itself; a software backend has no token, so it calls this.
506 ///
507 /// # Errors
508 ///
509 /// Returns [`BackendError::OperationNotPermitted`] when `usage` is absent
510 /// from the policy, naming what the key may do instead.
511 pub fn require(&self, usage: KeyUsages, operation: &str) -> Result<(), BackendError> {
512 if self.usages.contains(usage) {
513 return Ok(());
514 }
515 Err(BackendError::OperationNotPermitted(format!(
516 "this key may not {operation}: its policy allows {}. \
517 Declare the usage under `policy:` on the step that generates it.",
518 self.usages.describe()
519 )))
520 }
521}
522
523impl KeyPolicy {
524 /// What a key of this algorithm may do when the ceremony says nothing.
525 ///
526 /// A generation step should call this rather than [`Default`], which is
527 /// the signing default and gives a symmetric key two usages it can never
528 /// have. Rite holds a symmetric key as a key-encryption key and nothing
529 /// else, so wrap and unwrap is the whole of what one is for here.
530 ///
531 /// ```
532 /// use rite_sdk::{KeyAlgorithm, KeyPolicy, KeyUsages};
533 ///
534 /// let signing = KeyPolicy::default_for(KeyAlgorithm::EcdsaP256);
535 /// assert_eq!(signing.usages, KeyUsages::SIGN | KeyUsages::VERIFY);
536 ///
537 /// let kek = KeyPolicy::default_for(KeyAlgorithm::Aes256);
538 /// assert_eq!(kek.usages, KeyUsages::WRAP | KeyUsages::UNWRAP);
539 ///
540 /// // Everything else is the restrictive default either way.
541 /// assert!(!kek.extractable);
542 /// ```
543 #[must_use]
544 pub fn default_for(algorithm: KeyAlgorithm) -> Self {
545 if algorithm.is_symmetric() {
546 Self {
547 usages: KeyUsages::WRAP | KeyUsages::UNWRAP,
548 ..Self::default()
549 }
550 } else {
551 Self::default()
552 }
553 }
554}
555
556impl Default for KeyPolicy {
557 /// Most secure configuration for ceremony signing keys:
558 /// persistent=true, sensitive=true, extractable=false, sign+verify only.
559 fn default() -> Self {
560 Self {
561 persistent: true,
562 sensitive: true,
563 extractable: false,
564 wrap_with_trusted_only: false,
565 usages: KeyUsages::SIGN | KeyUsages::VERIFY,
566 }
567 }
568}
569
570/// Full specification for key generation or import.
571///
572/// Replaces the `(algorithm, label, slot_hint)` triplet with a structured type
573/// that can carry PKCS#11 policy attributes.
574#[derive(Debug, Clone, Serialize, Deserialize)]
575pub struct KeySpec {
576 /// Key algorithm.
577 pub algorithm: KeyAlgorithm,
578 /// Human-readable label.
579 pub label: String,
580 /// Security and usage policy.
581 pub policy: KeyPolicy,
582 /// Backend-specific location hint.
583 /// PIV: "9c" maps to Signature slot. PKCS#11: slot index or partition label.
584 /// Software: ignored.
585 pub location_hint: Option<String>,
586}
587
588/// PKCS#11 key security attributes read from an HSM after key generation.
589///
590/// Records HSM-managed attribute state as evidence of key provenance and
591/// policy compliance in the ceremony transcript.
592// Each bool maps directly to a named PKCS#11 boolean attribute (CKA_ALWAYS_SENSITIVE,
593// CKA_NEVER_EXTRACTABLE, CKA_SENSITIVE, CKA_EXTRACTABLE, CKA_WRAP_WITH_TRUSTED).
594// These are distinct concepts; collapsing them into enums would obscure the PKCS#11 semantics.
595#[allow(clippy::struct_excessive_bools)]
596#[derive(Debug, Clone, Serialize, Deserialize)]
597pub struct KeySecurityAttributes {
598 /// Key was ALWAYS sensitive: never had `CKA_SENSITIVE=false` since creation.
599 /// This is the primary provenance indicator for PKCS#11 keys.
600 pub always_sensitive: bool,
601 /// Key was NEVER extractable: never had `CKA_EXTRACTABLE=true` since creation.
602 pub never_extractable: bool,
603 /// Current value of `CKA_SENSITIVE`.
604 pub sensitive: bool,
605 /// Current value of `CKA_EXTRACTABLE`.
606 pub extractable: bool,
607 /// Key can only be wrapped by a key with `CKA_TRUSTED=true` (`CKA_WRAP_WITH_TRUSTED`).
608 pub wrap_with_trusted_only: bool,
609 /// What operations this key is permitted to perform (read from key object).
610 pub usages: KeyUsages,
611}
612
613/// Signature algorithm.
614#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
615#[serde(into = "String", try_from = "String")]
616#[non_exhaustive]
617pub enum SignAlgorithm {
618 /// RSASSA-PKCS1-v1_5 with SHA-256.
619 RsaPkcs1Sha256,
620 /// RSASSA-PSS with SHA-256.
621 RsaPssSha256,
622 /// ECDSA with SHA-256.
623 EcdsaSha256,
624 /// ECDSA with SHA-384.
625 EcdsaSha384,
626 /// Ed25519 (pure `EdDSA`, no hash function).
627 Ed25519,
628 /// ML-DSA-44 (pure, no pre-hash). FIPS 204.
629 MlDsa44,
630 /// ML-DSA-65 (pure, no pre-hash). FIPS 204.
631 MlDsa65,
632 /// ML-DSA-87 (pure, no pre-hash). FIPS 204.
633 MlDsa87,
634}
635
636impl SignAlgorithm {
637 /// A representative key algorithm for this signature algorithm.
638 ///
639 /// Used where a signature request must be turned into a concrete key
640 /// algorithm: selecting a card algorithm, or minting a stand-in key for a
641 /// rehearsal. Keeping that mapping here means those callers cannot drift
642 /// apart from each other.
643 ///
644 /// The mapping is deliberately lossy for RSA: both RSA schemes answer
645 /// `Rsa2048`, because a signature algorithm does not name a modulus size.
646 /// Use [`accepts_key`](Self::accepts_key) to test whether a key may be used
647 /// with this algorithm; the equality `alg.key_algorithm() == key` is not
648 /// that test and rejects RSA-4096 keys.
649 #[must_use]
650 pub fn key_algorithm(self) -> KeyAlgorithm {
651 match self {
652 SignAlgorithm::RsaPkcs1Sha256 | SignAlgorithm::RsaPssSha256 => KeyAlgorithm::Rsa2048,
653 SignAlgorithm::EcdsaSha256 => KeyAlgorithm::EcdsaP256,
654 SignAlgorithm::EcdsaSha384 => KeyAlgorithm::EcdsaP384,
655 SignAlgorithm::Ed25519 => KeyAlgorithm::Ed25519,
656 SignAlgorithm::MlDsa44 => KeyAlgorithm::MlDsa44,
657 SignAlgorithm::MlDsa65 => KeyAlgorithm::MlDsa65,
658 SignAlgorithm::MlDsa87 => KeyAlgorithm::MlDsa87,
659 }
660 }
661
662 /// Whether a key of `key_algorithm` may be used with this signature algorithm.
663 ///
664 /// The compatibility check every signing backend needs, kept in one place.
665 /// Curve and parameter set are pinned: ECDSA-SHA256 will not take a P-384
666 /// key. Only the RSA schemes span more than one key algorithm, since they
667 /// are defined for any modulus size.
668 #[must_use]
669 pub fn accepts_key(self, key_algorithm: KeyAlgorithm) -> bool {
670 match self {
671 SignAlgorithm::RsaPkcs1Sha256 | SignAlgorithm::RsaPssSha256 => {
672 matches!(key_algorithm, KeyAlgorithm::Rsa2048 | KeyAlgorithm::Rsa4096)
673 }
674 // Every other scheme pins exactly one key algorithm, already named
675 // by `key_algorithm`. Reuse it rather than repeating the pairing.
676 SignAlgorithm::EcdsaSha256
677 | SignAlgorithm::EcdsaSha384
678 | SignAlgorithm::Ed25519
679 | SignAlgorithm::MlDsa44
680 | SignAlgorithm::MlDsa65
681 | SignAlgorithm::MlDsa87 => self.key_algorithm() == key_algorithm,
682 }
683 }
684}
685
686impl fmt::Display for SignAlgorithm {
687 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
688 match self {
689 SignAlgorithm::RsaPkcs1Sha256 => write!(f, "RSA-PKCS1-SHA256"),
690 SignAlgorithm::RsaPssSha256 => write!(f, "RSA-PSS-SHA256"),
691 SignAlgorithm::EcdsaSha256 => write!(f, "ECDSA-SHA256"),
692 SignAlgorithm::EcdsaSha384 => write!(f, "ECDSA-SHA384"),
693 SignAlgorithm::Ed25519 => write!(f, "Ed25519"),
694 SignAlgorithm::MlDsa44 => write!(f, "ML-DSA-44"),
695 SignAlgorithm::MlDsa65 => write!(f, "ML-DSA-65"),
696 SignAlgorithm::MlDsa87 => write!(f, "ML-DSA-87"),
697 }
698 }
699}
700
701impl std::str::FromStr for SignAlgorithm {
702 type Err = ParseError;
703
704 fn from_str(s: &str) -> Result<Self, Self::Err> {
705 match s {
706 "RSA-PKCS1-SHA256" => Ok(Self::RsaPkcs1Sha256),
707 "RSA-PSS-SHA256" => Ok(Self::RsaPssSha256),
708 "ECDSA-SHA256" => Ok(Self::EcdsaSha256),
709 "ECDSA-SHA384" => Ok(Self::EcdsaSha384),
710 "Ed25519" => Ok(Self::Ed25519),
711 "ML-DSA-44" => Ok(Self::MlDsa44),
712 "ML-DSA-65" => Ok(Self::MlDsa65),
713 "ML-DSA-87" => Ok(Self::MlDsa87),
714 _ => Err(ParseError(s.to_owned())),
715 }
716 }
717}
718
719impl From<SignAlgorithm> for String {
720 fn from(a: SignAlgorithm) -> String {
721 a.to_string()
722 }
723}
724
725impl TryFrom<String> for SignAlgorithm {
726 type Error = ParseError;
727
728 fn try_from(s: String) -> Result<Self, Self::Error> {
729 s.parse()
730 }
731}
732
733/// An ASN.1 object identifier in dotted-decimal form.
734///
735/// Wrapping evidence is a set of algorithm identifiers read back out of the
736/// produced artifact, so the OID is the value that gets recorded. Held as text
737/// because that is what a transcript reader compares against a registry.
738#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
739#[serde(into = "String", try_from = "String")]
740pub struct Oid(String);
741
742impl Oid {
743 /// Parse a dotted-decimal OID.
744 ///
745 /// # Errors
746 ///
747 /// Returns [`ParseError`] unless the value is two or more arcs of decimal
748 /// digits separated by dots.
749 pub fn new(value: &str) -> Result<Self, ParseError> {
750 let arcs: Vec<&str> = value.split('.').collect();
751 let well_formed = arcs.len() >= 2
752 && arcs
753 .iter()
754 .all(|arc| !arc.is_empty() && arc.bytes().all(|b| b.is_ascii_digit()));
755 if well_formed {
756 Ok(Self(value.to_owned()))
757 } else {
758 Err(ParseError(value.to_owned()))
759 }
760 }
761
762 /// The dotted-decimal form.
763 #[must_use]
764 pub fn as_str(&self) -> &str {
765 &self.0
766 }
767}
768
769impl fmt::Display for Oid {
770 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
771 f.write_str(&self.0)
772 }
773}
774
775impl From<Oid> for String {
776 fn from(oid: Oid) -> String {
777 oid.0
778 }
779}
780
781impl TryFrom<String> for Oid {
782 type Error = ParseError;
783
784 fn try_from(s: String) -> Result<Self, Self::Error> {
785 Oid::new(&s)
786 }
787}
788
789/// Well-known OIDs the wrapping paths produce or check.
790pub mod oid {
791 /// `rsaEncryption`, RSAES-PKCS1-v1.5 key transport (RFC 8017).
792 pub const RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1";
793 /// `id-RSAES-OAEP` (RFC 8017).
794 pub const RSAES_OAEP: &str = "1.2.840.113549.1.1.7";
795 /// `dhSinglePass-stdDH-sha1kdf-scheme`, one-pass ECDH with the X9.63 KDF
796 /// over SHA-1 (RFC 5753).
797 pub const DH_SINGLE_PASS_STDDH_SHA1KDF: &str = "1.3.133.16.840.63.0.2";
798 /// `id-aes256-GCM` (RFC 5084).
799 pub const AES_256_GCM: &str = "2.16.840.1.101.3.4.1.46";
800 /// `id-aes256-CBC` (RFC 3565).
801 pub const AES_256_CBC: &str = "2.16.840.1.101.3.4.1.42";
802 /// `id-aes256-wrap`, AES Key Wrap with a 256-bit KEK (RFC 3394).
803 pub const AES_256_WRAP: &str = "2.16.840.1.101.3.4.1.45";
804 /// `id-aes128-wrap`, AES Key Wrap with a 128-bit KEK (RFC 3394).
805 pub const AES_128_WRAP: &str = "2.16.840.1.101.3.4.1.5";
806 /// `id-aes256-wrap-pad`, AES Key Wrap with Padding (RFC 5649).
807 pub const AES_256_WRAP_PAD: &str = "2.16.840.1.101.3.4.1.48";
808 /// `id-aes128-wrap-pad`, AES Key Wrap with Padding (RFC 5649).
809 pub const AES_128_WRAP_PAD: &str = "2.16.840.1.101.3.4.1.8";
810}
811
812/// How a CMS structure conveys the content-encryption key to its recipient.
813///
814/// The variants are the `RecipientInfo` CHOICE of RFC 5652 §6.2, plus the
815/// KEM alternative RFC 9629 carries inside `OtherRecipientInfo`.
816#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
817#[serde(rename_all = "snake_case")]
818#[non_exhaustive]
819pub enum RecipientInfoKind {
820 /// `KeyTransRecipientInfo`: the CEK is encrypted under the recipient's
821 /// public key.
822 Ktri,
823 /// `KeyAgreeRecipientInfo`: a shared secret is agreed with the recipient's
824 /// key, run through a KDF, and used to wrap the CEK.
825 Kari,
826 /// `KEKRecipientInfo`: the CEK is wrapped under a previously shared
827 /// symmetric key.
828 Kekri,
829 /// `KEMRecipientInfo` (RFC 9629), carried as `OtherRecipientInfo`.
830 Kemri,
831 /// `PasswordRecipientInfo`.
832 Pwri,
833 /// An `OtherRecipientInfo` this build does not recognise.
834 Ori,
835}
836
837impl fmt::Display for RecipientInfoKind {
838 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
839 match self {
840 RecipientInfoKind::Ktri => write!(f, "ktri"),
841 RecipientInfoKind::Kari => write!(f, "kari"),
842 RecipientInfoKind::Kekri => write!(f, "kekri"),
843 RecipientInfoKind::Kemri => write!(f, "kemri"),
844 RecipientInfoKind::Pwri => write!(f, "pwri"),
845 RecipientInfoKind::Ori => write!(f, "ori"),
846 }
847 }
848}
849
850/// What a wrap actually did.
851///
852/// The scheme a step requests names a family. The algorithms inside it move
853/// independently: the recipient's key type selects the encapsulation, the
854/// content cipher selects the KEK size, and the KDF digest is a library
855/// default rather than a property of any key.
856///
857/// Where the scheme is self-describing this is an observation, re-derivable
858/// from the blob alone, which is what makes it evidence. Where it is not, it
859/// is what the backend invoked. [`WrapScheme::is_self_describing`] is the
860/// difference, and `rite verify` reports the two differently.
861#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
862#[non_exhaustive]
863pub struct WrapDescription {
864 /// How the CEK reaches the recipient, for a scheme built on CMS.
865 ///
866 /// Absent for a raw mechanism, whose output carries no `RecipientInfo`
867 /// and no other ASN.1 structure.
868 #[serde(default, skip_serializing_if = "Option::is_none")]
869 pub recipient_info: Option<RecipientInfoKind>,
870 /// The key-encryption algorithm: key transport for `ktri`, the
871 /// key-agreement scheme for `kari`.
872 pub key_encryption_oid: Oid,
873 /// The KDF, where the encapsulation derives a KEK.
874 #[serde(default, skip_serializing_if = "Option::is_none")]
875 pub kdf_oid: Option<Oid>,
876 /// The algorithm wrapping the CEK under the derived KEK.
877 #[serde(default, skip_serializing_if = "Option::is_none")]
878 pub kek_wrap_oid: Option<Oid>,
879 /// The content cipher, for schemes that encrypt the key as CMS content.
880 #[serde(default, skip_serializing_if = "Option::is_none")]
881 pub content_encryption_oid: Option<Oid>,
882}
883
884impl WrapDescription {
885 /// Describe a CMS wrap by its recipient info and key-encryption algorithm.
886 #[must_use]
887 pub fn new(recipient_info: RecipientInfoKind, key_encryption_oid: Oid) -> Self {
888 Self {
889 recipient_info: Some(recipient_info),
890 key_encryption_oid,
891 kdf_oid: None,
892 kek_wrap_oid: None,
893 content_encryption_oid: None,
894 }
895 }
896
897 /// Describe a raw mechanism, which has no CMS structure around it.
898 #[must_use]
899 pub fn raw(key_encryption_oid: Oid) -> Self {
900 Self {
901 recipient_info: None,
902 key_encryption_oid,
903 kdf_oid: None,
904 kek_wrap_oid: None,
905 content_encryption_oid: None,
906 }
907 }
908
909 /// Record the KDF the encapsulation ran.
910 #[must_use]
911 pub fn with_kdf(mut self, oid: Oid) -> Self {
912 self.kdf_oid = Some(oid);
913 self
914 }
915
916 /// Record the algorithm that wrapped the CEK under the derived KEK.
917 #[must_use]
918 pub fn with_kek_wrap(mut self, oid: Oid) -> Self {
919 self.kek_wrap_oid = Some(oid);
920 self
921 }
922
923 /// Record the content cipher.
924 #[must_use]
925 pub fn with_content_encryption(mut self, oid: Oid) -> Self {
926 self.content_encryption_oid = Some(oid);
927 self
928 }
929
930 /// Whether the CMS content was encrypted under an authenticated cipher.
931 ///
932 /// True only for AES-256-GCM, the one authenticated cipher Rite writes, so
933 /// a foreign blob under another AEAD reads as unauthenticated here. It asks
934 /// about a CMS content cipher and nothing else, so it is `false` for every
935 /// raw mechanism, including ones that authenticate by other means.
936 #[must_use]
937 pub fn content_is_authenticated(&self) -> bool {
938 self.content_encryption_oid
939 .as_ref()
940 .is_some_and(|oid| oid.as_str() == oid::AES_256_GCM)
941 }
942}
943
944/// The wrapping scheme a step requests, or a backend reports.
945///
946/// A scheme names a container and the parts of it Rite fixes. What varies
947/// with the recipient is recorded in [`WrapDescription`] instead.
948#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
949#[serde(into = "String", try_from = "String")]
950#[non_exhaustive]
951pub enum WrapScheme {
952 /// CMS `AuthEnvelopedData` (RFC 5083) with AES-256-GCM content
953 /// encryption. Output: CMS `ContentInfo` DER.
954 ///
955 /// The encapsulation follows the recipient key: RSA takes key transport,
956 /// EC takes the RFC 5753 key-agreement path, and a KEM recipient takes the
957 /// RFC 9629 key-encapsulation path.
958 ///
959 /// `CMS-RSA-CBC`, the `EnvelopedData` variant with AES-256-CBC, was
960 /// removed. It carried no integrity protection, so unwrapping imported
961 /// whatever decrypted; PCI PIN v3.1 requirement 18-3 bans unauthenticated
962 /// key encryption. Restoring it would take an authenticated construction
963 /// around it, which is what the deployments that do use CBC (X9 TR-34,
964 /// AD CS key archival) wrap it in.
965 CmsAes256Gcm,
966 /// RSAES-OAEP with SHA-256, as raw ciphertext (RFC 8017).
967 ///
968 /// Output is exactly the modulus size and describes nothing about itself.
969 /// The payload ceiling is `k - 2*hLen - 2`: 190 bytes under RSA-2048, 446
970 /// under RSA-4096, which fits a symmetric key or an EC private key and
971 /// never fits an RSA one. [`Self::RsaAesKeyWrapSha256`] exists for that.
972 RsaOaepSha256,
973 /// RSA-AES key wrap with SHA-256 (PKCS#11 `CKM_RSA_AES_KEY_WRAP`).
974 ///
975 /// An ephemeral AES key under RSAES-OAEP, concatenated with the payload
976 /// under AES-KWP (RFC 5649). The OAEP part comes first and is exactly the
977 /// modulus size, which is how a recipient splits the two.
978 ///
979 /// This is the shape cloud KMS import expects, and it carries no size
980 /// ceiling. The digest is in the name because it is not recoverable from
981 /// the bytes and recipients differ: AWS names both `RSA_AES_KEY_WRAP_SHA_256`
982 /// and `RSA_AES_KEY_WRAP_SHA_1`, and Azure BYOK specifies SHA-1.
983 RsaAesKeyWrapSha256,
984 /// AES Key Wrap under a 128-bit KEK (RFC 3394, PKCS#11
985 /// `CKM_AES_KEY_WRAP`).
986 ///
987 /// The payload must be a multiple of 8 bytes and at least 16, which a
988 /// PKCS#8 private key is not, so this carries another symmetric key and
989 /// little else. [`Self::Aes128Kwp`] has no such rule.
990 Aes128Kw,
991 /// AES Key Wrap under a 256-bit KEK (RFC 3394).
992 Aes256Kw,
993 /// AES Key Wrap with Padding under a 128-bit KEK (RFC 5649, PKCS#11
994 /// `CKM_AES_KEY_WRAP_KWP`).
995 ///
996 /// Carries a payload of any length. This is what Thales recommends over
997 /// CBC for wrapping, and what `OpenStack` Barbican moved to in 2024 after
998 /// its hardcoded `CKM_AES_CBC_PAD` broke against Luna firmware in FIPS
999 /// mode.
1000 Aes128Kwp,
1001 /// AES Key Wrap with Padding under a 256-bit KEK (RFC 5649).
1002 Aes256Kwp,
1003}
1004
1005impl WrapScheme {
1006 /// The description this scheme fixes, where it fixes one.
1007 ///
1008 /// `None` for a self-describing scheme: what a CMS wrap did follows the
1009 /// recipient key, so it is read out of the artifact rather than known in
1010 /// advance. A raw mechanism has no such freedom, which is the same fact
1011 /// that makes its description an assertion.
1012 ///
1013 /// This is the one place that says what a raw scheme produces. A backend
1014 /// builds its description from here rather than restating the OIDs, and
1015 /// [`permits`](Self::permits) compares against it, so the two cannot
1016 /// disagree.
1017 #[must_use]
1018 pub fn fixed_description(self) -> Option<WrapDescription> {
1019 // Constructed directly rather than through the validating `Oid::new`:
1020 // these are this module's own constants, so there is no input to
1021 // reject and no error a caller could act on.
1022 let known = |value: &str| Oid(value.to_owned());
1023 match self {
1024 WrapScheme::CmsAes256Gcm => None,
1025 WrapScheme::RsaOaepSha256 => Some(WrapDescription::raw(known(oid::RSAES_OAEP))),
1026 WrapScheme::Aes128Kw => Some(WrapDescription::raw(known(oid::AES_128_WRAP))),
1027 WrapScheme::Aes256Kw => Some(WrapDescription::raw(known(oid::AES_256_WRAP))),
1028 WrapScheme::Aes128Kwp => Some(WrapDescription::raw(known(oid::AES_128_WRAP_PAD))),
1029 WrapScheme::Aes256Kwp => Some(WrapDescription::raw(known(oid::AES_256_WRAP_PAD))),
1030 WrapScheme::RsaAesKeyWrapSha256 => Some(
1031 WrapDescription::raw(known(oid::RSAES_OAEP))
1032 .with_kek_wrap(known(oid::AES_256_WRAP_PAD)),
1033 ),
1034 }
1035 }
1036
1037 /// The one key algorithm this scheme wraps under, where it names one.
1038 ///
1039 /// ```
1040 /// use rite_sdk::{KeyAlgorithm, WrapScheme};
1041 ///
1042 /// assert_eq!(WrapScheme::Aes256Kwp.required_kek(), Some(KeyAlgorithm::Aes256));
1043 /// // CMS admits a family, so it names none.
1044 /// assert_eq!(WrapScheme::CmsAes256Gcm.required_kek(), None);
1045 /// ```
1046 ///
1047 /// `None` where the scheme admits a family rather than a single algorithm.
1048 /// The AES schemes name a KEK size, because the RFC 3394 and RFC 5649
1049 /// object identifiers are per-size and [`fixed_description`](Self::fixed_description)
1050 /// has only the scheme to go on. Naming it is also what makes a ceremony
1051 /// that expects a 256-bit KEK refuse a 128-bit one instead of wrapping
1052 /// under it in silence.
1053 #[must_use]
1054 pub fn required_kek(self) -> Option<KeyAlgorithm> {
1055 match self {
1056 WrapScheme::CmsAes256Gcm
1057 | WrapScheme::RsaOaepSha256
1058 | WrapScheme::RsaAesKeyWrapSha256 => None,
1059 WrapScheme::Aes128Kw | WrapScheme::Aes128Kwp => Some(KeyAlgorithm::Aes128),
1060 WrapScheme::Aes256Kw | WrapScheme::Aes256Kwp => Some(KeyAlgorithm::Aes256),
1061 }
1062 }
1063
1064 /// Whether a key of this algorithm can receive a wrap under this scheme.
1065 ///
1066 /// Narrower than [`KeyAlgorithm::can_receive_wrap`], which answers for CMS,
1067 /// where the recipient key selects the encapsulation. The RSA mechanisms
1068 /// accept nothing but RSA, and the AES mechanisms nothing but the KEK size
1069 /// in their name.
1070 ///
1071 /// A symmetric key passes here and still cannot arrive as `recipient:`,
1072 /// since that path takes a public key and a symmetric key has none. An
1073 /// AES scheme is reachable only under `wrapping_key:`.
1074 #[must_use]
1075 pub fn accepts_recipient(self, algorithm: KeyAlgorithm) -> bool {
1076 match self {
1077 WrapScheme::CmsAes256Gcm => algorithm.can_receive_wrap(),
1078 WrapScheme::RsaOaepSha256 | WrapScheme::RsaAesKeyWrapSha256 => match algorithm {
1079 KeyAlgorithm::Rsa2048 | KeyAlgorithm::Rsa4096 => true,
1080 KeyAlgorithm::EcdsaP256
1081 | KeyAlgorithm::EcdsaP384
1082 | KeyAlgorithm::Ed25519
1083 | KeyAlgorithm::MlDsa44
1084 | KeyAlgorithm::MlDsa65
1085 | KeyAlgorithm::MlDsa87
1086 | KeyAlgorithm::MlKem512
1087 | KeyAlgorithm::MlKem768
1088 | KeyAlgorithm::MlKem1024
1089 | KeyAlgorithm::Aes128
1090 | KeyAlgorithm::Aes256 => false,
1091 },
1092 WrapScheme::Aes128Kw
1093 | WrapScheme::Aes256Kw
1094 | WrapScheme::Aes128Kwp
1095 | WrapScheme::Aes256Kwp => self.required_kek() == Some(algorithm),
1096 }
1097 }
1098
1099 /// Whether `description` is one this scheme admits.
1100 ///
1101 /// The check is what keeps a recorded scheme from contradicting the
1102 /// artifact it labels. For a self-describing scheme it is a structural
1103 /// test against what was read out of the bytes; for a raw mechanism the
1104 /// description is fixed, so equality is the whole test.
1105 #[must_use]
1106 pub fn permits(self, description: &WrapDescription) -> bool {
1107 if let Some(fixed) = self.fixed_description() {
1108 return *description == fixed;
1109 }
1110 // `kekri` joins the three public-key encapsulations: the recipient is
1111 // a key both sides hold rather than one the wrap reaches by public
1112 // key, and the container and its guarantees are the same either way.
1113 let encapsulation_fits = matches!(
1114 description.recipient_info,
1115 Some(
1116 RecipientInfoKind::Ktri
1117 | RecipientInfoKind::Kari
1118 | RecipientInfoKind::Kemri
1119 | RecipientInfoKind::Kekri
1120 )
1121 );
1122 description.content_is_authenticated() && encapsulation_fits
1123 }
1124
1125 /// Whether a verifier can re-derive the wrap's algorithms from the bytes.
1126 ///
1127 /// This is the line between evidence and assertion. A CMS artifact carries
1128 /// its own algorithm identifiers, so `rite verify` reads them back and
1129 /// compares them against the transcript; raw mechanism output carries
1130 /// nothing, so the recorded algorithms are what the backend reports having
1131 /// invoked and no offline check can confirm them.
1132 ///
1133 /// Matched exhaustively on purpose: a scheme added without answering this
1134 /// would otherwise default into being treated as evidence.
1135 #[must_use]
1136 pub fn is_self_describing(self) -> bool {
1137 match self {
1138 WrapScheme::CmsAes256Gcm => true,
1139 WrapScheme::RsaOaepSha256
1140 | WrapScheme::RsaAesKeyWrapSha256
1141 | WrapScheme::Aes128Kw
1142 | WrapScheme::Aes256Kw
1143 | WrapScheme::Aes128Kwp
1144 | WrapScheme::Aes256Kwp => false,
1145 }
1146 }
1147}
1148
1149impl fmt::Display for WrapScheme {
1150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1151 match self {
1152 WrapScheme::CmsAes256Gcm => write!(f, "CMS-AES-256-GCM"),
1153 WrapScheme::RsaOaepSha256 => write!(f, "RSA-OAEP-SHA256"),
1154 WrapScheme::RsaAesKeyWrapSha256 => write!(f, "RSA-AES-KEY-WRAP-SHA256"),
1155 WrapScheme::Aes128Kw => write!(f, "AES-128-KW"),
1156 WrapScheme::Aes256Kw => write!(f, "AES-256-KW"),
1157 WrapScheme::Aes128Kwp => write!(f, "AES-128-KWP"),
1158 WrapScheme::Aes256Kwp => write!(f, "AES-256-KWP"),
1159 }
1160 }
1161}
1162
1163impl std::str::FromStr for WrapScheme {
1164 type Err = ParseError;
1165
1166 fn from_str(s: &str) -> Result<Self, Self::Err> {
1167 match s {
1168 "CMS-AES-256-GCM" => Ok(Self::CmsAes256Gcm),
1169 "RSA-OAEP-SHA256" => Ok(Self::RsaOaepSha256),
1170 "RSA-AES-KEY-WRAP-SHA256" => Ok(Self::RsaAesKeyWrapSha256),
1171 "AES-128-KW" => Ok(Self::Aes128Kw),
1172 "AES-256-KW" => Ok(Self::Aes256Kw),
1173 "AES-128-KWP" => Ok(Self::Aes128Kwp),
1174 "AES-256-KWP" => Ok(Self::Aes256Kwp),
1175 _ => Err(ParseError(s.to_owned())),
1176 }
1177 }
1178}
1179
1180impl From<WrapScheme> for String {
1181 fn from(a: WrapScheme) -> String {
1182 a.to_string()
1183 }
1184}
1185
1186impl TryFrom<String> for WrapScheme {
1187 type Error = ParseError;
1188
1189 fn try_from(s: String) -> Result<Self, Self::Error> {
1190 s.parse()
1191 }
1192}
1193
1194/// A wrapped key, the scheme that produced it, and what that wrap did.
1195///
1196/// The three are constructed together and read together: a scheme that does
1197/// not admit the description would label the bytes with something the bytes
1198/// contradict.
1199#[derive(Debug, Clone, Serialize, Deserialize)]
1200#[serde(try_from = "WrappedKeyRepr")]
1201pub struct WrappedKey {
1202 scheme: WrapScheme,
1203 description: WrapDescription,
1204 data: Vec<u8>,
1205}
1206
1207/// Deserialization shape for [`WrappedKey`], re-checked on the way in.
1208#[derive(Deserialize)]
1209struct WrappedKeyRepr {
1210 scheme: WrapScheme,
1211 description: WrapDescription,
1212 data: Vec<u8>,
1213}
1214
1215impl TryFrom<WrappedKeyRepr> for WrappedKey {
1216 type Error = IncoherentWrap;
1217
1218 fn try_from(repr: WrappedKeyRepr) -> Result<Self, Self::Error> {
1219 WrappedKey::new(repr.scheme, repr.description, repr.data)
1220 }
1221}
1222
1223/// A wrapped key was labelled with a scheme its own description contradicts.
1224#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1225#[error("scheme {scheme} does not admit this wrap: {description:?}")]
1226pub struct IncoherentWrap {
1227 /// The scheme the wrap was labelled with.
1228 pub scheme: WrapScheme,
1229 /// What the artifact says was done.
1230 pub description: WrapDescription,
1231}
1232
1233impl WrappedKey {
1234 /// Pair wrapped bytes with the scheme and the description of the wrap.
1235 ///
1236 /// # Errors
1237 ///
1238 /// Returns [`IncoherentWrap`] if the scheme does not admit the
1239 /// description.
1240 pub fn new(
1241 scheme: WrapScheme,
1242 description: WrapDescription,
1243 data: Vec<u8>,
1244 ) -> Result<Self, IncoherentWrap> {
1245 if scheme.permits(&description) {
1246 Ok(Self {
1247 scheme,
1248 description,
1249 data,
1250 })
1251 } else {
1252 Err(IncoherentWrap {
1253 scheme,
1254 description,
1255 })
1256 }
1257 }
1258
1259 /// The scheme this key was wrapped under.
1260 #[must_use]
1261 pub fn scheme(&self) -> WrapScheme {
1262 self.scheme
1263 }
1264
1265 /// What the wrap did, read back from the artifact.
1266 #[must_use]
1267 pub fn description(&self) -> &WrapDescription {
1268 &self.description
1269 }
1270
1271 /// The wrapped bytes.
1272 #[must_use]
1273 pub fn data(&self) -> &[u8] {
1274 &self.data
1275 }
1276}
1277
1278/// The kind of attestation evidence a backend can produce.
1279#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1280#[serde(rename_all = "snake_case")]
1281#[non_exhaustive]
1282pub enum AttestationKind {
1283 /// Vendor cert chain proving on-device generation. Independently verifiable
1284 /// against a public manufacturer root CA.
1285 /// Examples: `YubiKey` F9 attestation, Thales Luna PKC (Public Key Confirmation).
1286 HardwareCertChain,
1287 /// TPM signed quote over PCR values with nonce. Proves platform state.
1288 TpmQuote,
1289 /// PKCS#11 attribute flags: `CKA_ALWAYS_SENSITIVE=true` + `CKA_NEVER_EXTRACTABLE=true`.
1290 /// Trustworthy within the HSM's trust boundary but not independently
1291 /// cryptographically verifiable.
1292 Pkcs11Attributes,
1293}
1294
1295/// Structured attestation evidence with a discriminant kind.
1296///
1297/// The `kind` field tells consumers what they're looking at and how to verify it.
1298#[derive(Debug, Clone, Serialize, Deserialize)]
1299pub struct Attestation {
1300 /// What kind of attestation this is.
1301 pub kind: AttestationKind,
1302 /// DER-encoded certificate chain. Leaf first, then intermediates, then root (if known).
1303 /// Empty for `Pkcs11Attributes` kind.
1304 pub certificates: Vec<Vec<u8>>,
1305 /// Raw signature bytes (if applicable). None for attribute-based attestation.
1306 pub signature: Option<Vec<u8>>,
1307 /// Additional metadata (key attributes, slot info, etc.) for transcript recording.
1308 pub metadata: serde_json::Value,
1309}
1310
1311/// Device information for platform attestation.
1312///
1313/// Collected from the system running the ceremony. Machine IDs are hashed
1314/// (SHA-256) for privacy before inclusion in evidence.
1315#[derive(Debug, Clone, Serialize, Deserialize)]
1316pub struct DeviceInfo {
1317 /// System hostname.
1318 pub hostname: String,
1319 /// Hashed machine ID (SHA-256, format: "sha256:hexhash").
1320 /// None if machine ID not available on this platform.
1321 pub machine_id: Option<String>,
1322 /// CPU model string.
1323 pub cpu_model: Option<String>,
1324 /// Operating system name.
1325 pub os_name: Option<String>,
1326 /// Operating system version.
1327 pub os_version: Option<String>,
1328 /// Kernel version.
1329 pub kernel_version: Option<String>,
1330}
1331
1332/// PIV key slot identifiers.
1333///
1334/// Defined by NIST SP 800-73-5 Part 1, Table 4b ("PIV Card Application Card
1335/// Command Interface: Key References"). These slot assignments are fixed by
1336/// the standard and MUST NOT be changed.
1337///
1338/// # Standard references
1339/// - NIST SP 800-73-5 Part 1, §3.1.2: Key References
1340/// - NIST SP 800-73-5 Part 2, §3.2: GENERAL AUTHENTICATE
1341#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1342pub enum PivSlot {
1343 /// Slot 9A: PIV Authentication Key.
1344 /// Used for card/cardholder authentication (e.g. system login).
1345 /// PIN required once per session.
1346 /// [NIST SP 800-73-5 Part 1, §3.1.2]
1347 Authentication,
1348
1349 /// Slot 9C: Digital Signature Key.
1350 /// Used for document/code signing. PIN required before EVERY
1351 /// private key operation ("PIN Always" access rule).
1352 /// Data to be signed is hashed off-card.
1353 /// [NIST SP 800-73-5 Part 1, §3.1.2; Part 2, §3.2.4]
1354 Signature,
1355
1356 /// Slot 9D: Key Management Key.
1357 /// Used for key establishment (encryption/decryption).
1358 /// PIN required once per session.
1359 /// [NIST SP 800-73-5 Part 1, §3.1.2]
1360 KeyManagement,
1361
1362 /// Slot 9E: Card Authentication Key.
1363 /// Used for physical access (e.g. PIV-enabled door locks).
1364 /// NO PIN required for private key operations.
1365 /// [NIST SP 800-73-5 Part 1, §3.1.2]
1366 CardAuthentication,
1367
1368 /// Retired Key Management Key slot.
1369 ///
1370 /// Holds a previously used Key Management key for decrypting historical
1371 /// documents. The inner value is an index in `0..=19`, where 0 corresponds
1372 /// to PIV key reference 0x82 and 19 corresponds to 0x95.
1373 ///
1374 /// Construct with [`PivSlot::retired`] to enforce the valid range.
1375 /// [NIST SP 800-73-5 Part 1, §3.1.2]
1376 Retired(u8),
1377}
1378
1379impl PivSlot {
1380 /// Create a retired key management slot from an index in `0..=19`.
1381 ///
1382 /// Returns `None` if `index` is out of the valid range.
1383 /// The 20 retired slots correspond to PIV key references 0x82–0x95.
1384 pub fn retired(index: u8) -> Option<Self> {
1385 (index <= 19).then_some(Self::Retired(index))
1386 }
1387}
1388
1389/// Unified certificate addressing across backend types.
1390///
1391/// PIV addresses certs by slot. PKCS#11 addresses cert objects by `CKA_LABEL` or `CKA_ID`.
1392/// Both are "find me the cert for this key"; the addressing differs by backend type.
1393#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1394#[non_exhaustive]
1395pub enum CertRef {
1396 /// PIV slot identifier (`PivBackend` and `YubikeyBackend`).
1397 PivSlot(PivSlot),
1398 /// Human-readable label string (PKCS#11 `CKA_LABEL`, or software backend).
1399 Label(String),
1400 /// Raw binary identifier (PKCS#11 `CKA_ID`, typically SHA-1 of public key).
1401 RawId(Vec<u8>),
1402}
1403
1404/// PIN access policy for PIV private key operations.
1405///
1406/// Standard PIV defines fixed PIN policies per slot (see `PivSlot` docs).
1407/// `YubiKey` extends this with per-key configurable policies.
1408///
1409/// # Standard references
1410/// - NIST SP 800-73-5 Part 2, §3.2: access rules per key reference
1411/// - Yubico PIV documentation: configurable PIN policies (vendor extension)
1412#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1413#[serde(rename_all = "snake_case")]
1414#[non_exhaustive]
1415pub enum PivPinPolicy {
1416 /// Use the slot's default policy per NIST SP 800-73.
1417 Default,
1418 /// No PIN required (vendor extension, not standard PIV).
1419 Never,
1420 /// PIN verified once per session.
1421 Once,
1422 /// PIN verified before every private key operation ("PIN Always").
1423 Always,
1424}
1425
1426/// Physical touch policy for private key operations.
1427///
1428/// This is a vendor extension (Yubico), not part of the NIST PIV standard.
1429/// Standard PIV cards do not have a touch sensor concept.
1430#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1431#[serde(rename_all = "snake_case")]
1432#[non_exhaustive]
1433pub enum PivTouchPolicy {
1434 /// Use the slot's default touch policy.
1435 Default,
1436 /// No touch required.
1437 Never,
1438 /// Touch required for every operation.
1439 Always,
1440 /// Touch cached for 15 seconds (Yubico-specific).
1441 Cached,
1442}
1443
1444/// Metadata about a populated PIV slot.
1445#[derive(Debug, Clone, Serialize, Deserialize)]
1446pub struct PivSlotInfo {
1447 /// The PIV slot identifier.
1448 pub slot: PivSlot,
1449 /// Key algorithm in this slot (if known).
1450 pub algorithm: Option<KeyAlgorithm>,
1451 /// Whether a certificate is stored in this slot.
1452 /// PIV associates one X.509 certificate with each key slot.
1453 /// [NIST SP 800-73-5 Part 1, §3.2]
1454 pub has_certificate: bool,
1455 /// Origin of the key in this slot.
1456 pub origin: PivKeyOrigin,
1457}
1458
1459/// Origin of a key in a PIV slot.
1460#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1461#[serde(rename_all = "snake_case")]
1462#[non_exhaustive]
1463pub enum PivKeyOrigin {
1464 /// Key was generated on the device (GENERATE ASYMMETRIC KEY PAIR command).
1465 /// [NIST SP 800-73-5 Part 2, §3.1]
1466 Generated,
1467 /// Key was imported from external material.
1468 Imported,
1469 /// Origin unknown (card doesn't report this information).
1470 Unknown,
1471}
1472
1473/// Device identity information for a PIV-compatible smart card.
1474#[derive(Debug, Clone, Serialize, Deserialize)]
1475pub struct PivDeviceInfo {
1476 /// Device serial number (vendor-specific, not standardized by NIST).
1477 pub serial: Option<String>,
1478 /// Firmware version string.
1479 pub firmware_version: Option<String>,
1480 /// Form factor description (e.g. "USB-A", "USB-C", "NFC").
1481 pub form_factor: Option<String>,
1482}
1483
1484/// `YubiKey`-specific slot metadata (vendor extension, not NIST PIV).
1485#[derive(Debug, Clone, Serialize, Deserialize)]
1486pub struct YubikeySlotMetadata {
1487 /// PIN policy configured for this slot.
1488 pub pin_policy: PivPinPolicy,
1489 /// Touch policy configured for this slot.
1490 pub touch_policy: PivTouchPolicy,
1491 /// Origin of the key in this slot.
1492 pub origin: PivKeyOrigin,
1493 /// Public key, if the slot reports one.
1494 pub public_key: Option<PublicKeyDer>,
1495}
1496
1497bitflags::bitflags! {
1498 /// PKCS#11 token capability flags. Maps to the `CKF_*` constants in the PKCS#11 standard.
1499 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1500 pub struct Pkcs11TokenFlags: u8 {
1501 /// Token requires login before cryptographic operations (`CKF_LOGIN_REQUIRED`).
1502 const LOGIN_REQUIRED = 0b0000_0001;
1503 /// User PIN has been initialized: `C_InitPIN` has been called (`CKF_USER_PIN_INITIALIZED`).
1504 const USER_PIN_INITIALIZED = 0b0000_0010;
1505 /// Token has been initialized: `C_InitToken` has been called (`CKF_TOKEN_INITIALIZED`).
1506 const TOKEN_INITIALIZED = 0b0000_0100;
1507 /// Token is write-protected (`CKF_WRITE_PROTECTED`).
1508 const WRITE_PROTECTED = 0b0000_1000;
1509 }
1510}
1511
1512/// PKCS#11 token information.
1513#[derive(Debug, Clone, Serialize, Deserialize)]
1514pub struct Pkcs11TokenInfo {
1515 /// Token label (padded to 32 bytes in PKCS#11, trimmed here).
1516 pub label: String,
1517 /// Manufacturer ID.
1518 pub manufacturer: String,
1519 /// Token model.
1520 pub model: String,
1521 /// Token serial number.
1522 pub serial: String,
1523 /// Firmware version string.
1524 pub firmware_version: String,
1525 /// Token capability flags.
1526 pub flags: Pkcs11TokenFlags,
1527}
1528
1529/// Opaque PKCS#11 mechanism identifier.
1530///
1531/// An enum would be incomplete (hundreds of `CKM_*` values, plus vendor extensions).
1532/// A string like `"CKM_RSA_PKCS_KEY_PAIR_GEN"` is honest about what we expose.
1533/// Used only for capability checking ("does this token support X?"), not dispatch.
1534#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1535#[serde(transparent)]
1536pub struct Pkcs11Mechanism(String);
1537
1538impl Pkcs11Mechanism {
1539 /// Create a new `Pkcs11Mechanism` from a `CKM_*` name string.
1540 pub fn new(name: impl Into<String>) -> Self {
1541 Self(name.into())
1542 }
1543
1544 /// Return the mechanism name as a string slice.
1545 pub fn as_str(&self) -> &str {
1546 &self.0
1547 }
1548}
1549
1550impl fmt::Display for Pkcs11Mechanism {
1551 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1552 write!(f, "{}", self.0)
1553 }
1554}
1555
1556/// TPM (Trusted Platform Module) information.
1557#[derive(Debug, Clone, Serialize, Deserialize)]
1558pub struct TpmInfo {
1559 /// TPM specification version (e.g., "2.0").
1560 pub version: String,
1561 /// TPM manufacturer identifier.
1562 pub manufacturer: String,
1563 /// Firmware version (if available).
1564 pub firmware_version: Option<String>,
1565}
1566
1567/// PCR (Platform Configuration Register) value.
1568///
1569/// The `value` field encodes both the algorithm and the hash in the format
1570/// `"algorithm:hexhash"` (e.g., `"sha256:abc123..."`).
1571#[derive(Debug, Clone, Serialize, Deserialize)]
1572pub struct PcrValue {
1573 /// PCR index (0-23 typically).
1574 pub index: u8,
1575 /// PCR value in the format `"algorithm:hexhash"` (e.g., `"sha256:abc123..."`).
1576 pub value: String,
1577}
1578
1579/// Backend configuration entry from a ceremony file.
1580///
1581/// The `provider` field identifies the backend by its self-declared name
1582/// (e.g., `"software"`, `"yubikey"`). The remaining YAML keys are flattened
1583/// into `extra` for backend-specific deserialization.
1584///
1585/// Each backend crate defines its own config struct and deserializes from
1586/// `extra` at startup. This inverts the prior design where the model listed
1587/// concrete backend variants; backends now declare themselves.
1588#[derive(Debug, Clone, Serialize, Deserialize)]
1589pub struct BackendConfig {
1590 /// Backend provider identifier as a lowercase string (e.g., `"software"`).
1591 pub provider: String,
1592 /// Backend-specific configuration key/value pairs.
1593 #[serde(flatten)]
1594 pub extra: serde_json::Value,
1595}
1596
1597#[cfg(test)]
1598mod tests {
1599 use super::*;
1600
1601 #[test]
1602 fn key_algorithm_serde_roundtrip() {
1603 // Serde uses Display strings via `serde(into/try_from)`. These are the canonical
1604 // strings for ceremony YAML `algorithm:` fields and transcripts.
1605 let cases: &[(KeyAlgorithm, &str)] = &[
1606 (KeyAlgorithm::Rsa2048, "\"RSA-2048\""),
1607 (KeyAlgorithm::Rsa4096, "\"RSA-4096\""),
1608 (KeyAlgorithm::EcdsaP256, "\"ECDSA-P256\""),
1609 (KeyAlgorithm::EcdsaP384, "\"ECDSA-P384\""),
1610 (KeyAlgorithm::Ed25519, "\"Ed25519\""),
1611 (KeyAlgorithm::MlDsa44, "\"ML-DSA-44\""),
1612 (KeyAlgorithm::MlDsa65, "\"ML-DSA-65\""),
1613 (KeyAlgorithm::MlDsa87, "\"ML-DSA-87\""),
1614 (KeyAlgorithm::MlKem512, "\"ML-KEM-512\""),
1615 (KeyAlgorithm::MlKem768, "\"ML-KEM-768\""),
1616 (KeyAlgorithm::MlKem1024, "\"ML-KEM-1024\""),
1617 (KeyAlgorithm::Aes128, "\"AES-128\""),
1618 (KeyAlgorithm::Aes256, "\"AES-256\""),
1619 ];
1620 for &(variant, expected) in cases {
1621 let serialized = serde_json::to_string(&variant).unwrap();
1622 assert_eq!(serialized, expected, "serialize {variant:?}");
1623 let deserialized: KeyAlgorithm = serde_json::from_str(expected).unwrap();
1624 assert_eq!(deserialized, variant, "deserialize {expected}");
1625 }
1626 }
1627
1628 #[test]
1629 fn sign_algorithm_serde_roundtrip() {
1630 // Serde uses Display strings via `serde(into/try_from)`. These are the canonical
1631 // strings for ceremony YAML `algorithm:` fields and transcripts.
1632 let cases: &[(SignAlgorithm, &str)] = &[
1633 (SignAlgorithm::RsaPkcs1Sha256, "\"RSA-PKCS1-SHA256\""),
1634 (SignAlgorithm::RsaPssSha256, "\"RSA-PSS-SHA256\""),
1635 (SignAlgorithm::EcdsaSha256, "\"ECDSA-SHA256\""),
1636 (SignAlgorithm::EcdsaSha384, "\"ECDSA-SHA384\""),
1637 (SignAlgorithm::Ed25519, "\"Ed25519\""),
1638 (SignAlgorithm::MlDsa44, "\"ML-DSA-44\""),
1639 (SignAlgorithm::MlDsa65, "\"ML-DSA-65\""),
1640 (SignAlgorithm::MlDsa87, "\"ML-DSA-87\""),
1641 ];
1642 for &(variant, expected) in cases {
1643 let serialized = serde_json::to_string(&variant).unwrap();
1644 assert_eq!(serialized, expected, "serialize {variant:?}");
1645 let deserialized: SignAlgorithm = serde_json::from_str(expected).unwrap();
1646 assert_eq!(deserialized, variant, "deserialize {expected}");
1647 }
1648 }
1649
1650 #[test]
1651 fn sign_algorithm_from_str_rejects_unknown() {
1652 assert!("RSA-PKCS1-SHA512".parse::<SignAlgorithm>().is_err());
1653 assert!("".parse::<SignAlgorithm>().is_err());
1654 assert!(
1655 "ecdsa-sha256".parse::<SignAlgorithm>().is_err(),
1656 "must be case-sensitive"
1657 );
1658 assert!(
1659 "ecdsa_sha256".parse::<SignAlgorithm>().is_err(),
1660 "the snake_case spelling is not accepted"
1661 );
1662 }
1663
1664 /// The default must be a signature algorithm the key can actually perform,
1665 /// or actions that derive one would hand backends an impossible request.
1666 #[test]
1667 fn every_default_sign_algorithm_accepts_its_own_key() {
1668 let signing_keys = [
1669 KeyAlgorithm::Rsa2048,
1670 KeyAlgorithm::Rsa4096,
1671 KeyAlgorithm::EcdsaP256,
1672 KeyAlgorithm::EcdsaP384,
1673 KeyAlgorithm::Ed25519,
1674 KeyAlgorithm::MlDsa44,
1675 KeyAlgorithm::MlDsa65,
1676 KeyAlgorithm::MlDsa87,
1677 ];
1678 for key_algorithm in signing_keys {
1679 let algorithm = key_algorithm
1680 .default_sign_algorithm()
1681 .unwrap_or_else(|| panic!("{key_algorithm} must have a default"));
1682 assert!(
1683 algorithm.accepts_key(key_algorithm),
1684 "{key_algorithm} defaults to {algorithm}, which rejects it"
1685 );
1686 }
1687
1688 // Symmetric keys sign nothing.
1689 assert!(KeyAlgorithm::Aes128.default_sign_algorithm().is_none());
1690 assert!(KeyAlgorithm::Aes256.default_sign_algorithm().is_none());
1691 }
1692
1693 #[test]
1694 fn sign_algorithm_accepts_key_spans_rsa_sizes_and_pins_everything_else() {
1695 // RSA signature schemes are defined for any modulus size, so both key
1696 // sizes are valid. This is what `key_algorithm()` cannot express.
1697 for algorithm in [SignAlgorithm::RsaPkcs1Sha256, SignAlgorithm::RsaPssSha256] {
1698 assert!(algorithm.accepts_key(KeyAlgorithm::Rsa2048));
1699 assert!(algorithm.accepts_key(KeyAlgorithm::Rsa4096));
1700 assert!(!algorithm.accepts_key(KeyAlgorithm::EcdsaP256));
1701 }
1702
1703 // Curves and ML-DSA parameter sets are pinned: a signature algorithm
1704 // names exactly one key algorithm and rejects its neighbours.
1705 assert!(SignAlgorithm::EcdsaSha256.accepts_key(KeyAlgorithm::EcdsaP256));
1706 assert!(!SignAlgorithm::EcdsaSha256.accepts_key(KeyAlgorithm::EcdsaP384));
1707 assert!(SignAlgorithm::MlDsa65.accepts_key(KeyAlgorithm::MlDsa65));
1708 assert!(!SignAlgorithm::MlDsa65.accepts_key(KeyAlgorithm::MlDsa87));
1709
1710 // A signing algorithm never accepts a symmetric key.
1711 assert!(!SignAlgorithm::Ed25519.accepts_key(KeyAlgorithm::Aes256));
1712 }
1713
1714 #[test]
1715 fn wrap_scheme_serde_roundtrip() {
1716 // Serde uses Display strings via `serde(into/try_from)`. These strings
1717 // appear in transcripts.
1718 let cases: &[(WrapScheme, &str)] = &[
1719 (WrapScheme::CmsAes256Gcm, "\"CMS-AES-256-GCM\""),
1720 (WrapScheme::RsaOaepSha256, "\"RSA-OAEP-SHA256\""),
1721 (
1722 WrapScheme::RsaAesKeyWrapSha256,
1723 "\"RSA-AES-KEY-WRAP-SHA256\"",
1724 ),
1725 (WrapScheme::Aes128Kw, "\"AES-128-KW\""),
1726 (WrapScheme::Aes256Kw, "\"AES-256-KW\""),
1727 (WrapScheme::Aes128Kwp, "\"AES-128-KWP\""),
1728 (WrapScheme::Aes256Kwp, "\"AES-256-KWP\""),
1729 ];
1730 for &(variant, expected) in cases {
1731 let serialized = serde_json::to_string(&variant).unwrap();
1732 assert_eq!(serialized, expected, "serialize {variant:?}");
1733 let deserialized: WrapScheme = serde_json::from_str(expected).unwrap();
1734 assert_eq!(deserialized, variant, "deserialize {expected}");
1735 }
1736 }
1737
1738 #[test]
1739 fn wrap_scheme_from_str_rejects_unknown() {
1740 assert!("CMS-RSA-XTS".parse::<WrapScheme>().is_err());
1741 assert!("".parse::<WrapScheme>().is_err());
1742 assert!(
1743 "cms-aes-256-gcm".parse::<WrapScheme>().is_err(),
1744 "must be case-sensitive"
1745 );
1746 assert!(
1747 "CMS-RSA-CBC".parse::<WrapScheme>().is_err(),
1748 "the unauthenticated CBC scheme is gone, not merely undocumented"
1749 );
1750 }
1751
1752 /// A CMS wrap to an RSA recipient, as OpenSSL 3.6 produces it.
1753 fn cms_ktri_gcm() -> WrapDescription {
1754 WrapDescription::new(
1755 RecipientInfoKind::Ktri,
1756 Oid::new(oid::RSA_ENCRYPTION).unwrap(),
1757 )
1758 .with_content_encryption(Oid::new(oid::AES_256_GCM).unwrap())
1759 }
1760
1761 #[test]
1762 fn oid_rejects_values_that_are_not_dotted_decimal() {
1763 assert!(Oid::new("2.16.840.1.101.3.4.1.46").is_ok());
1764 assert!(Oid::new("1.2").is_ok());
1765 assert!(Oid::new("1").is_err(), "a single arc is not an OID");
1766 assert!(
1767 Oid::new("1.2.").is_err(),
1768 "trailing dot leaves an empty arc"
1769 );
1770 assert!(Oid::new("1.2.a").is_err());
1771 assert!(Oid::new("").is_err());
1772 }
1773
1774 #[test]
1775 fn scheme_admits_only_a_matching_description() {
1776 assert!(WrapScheme::CmsAes256Gcm.permits(&cms_ktri_gcm()));
1777
1778 // Same container, CBC content: the scheme fixes GCM, so this is not it.
1779 let cbc = WrapDescription::new(
1780 RecipientInfoKind::Ktri,
1781 Oid::new(oid::RSA_ENCRYPTION).unwrap(),
1782 )
1783 .with_content_encryption(Oid::new(oid::AES_256_CBC).unwrap());
1784 assert!(!WrapScheme::CmsAes256Gcm.permits(&cbc));
1785
1786 // A symmetric KEK is the KEKRI encapsulation, which this scheme does
1787 // produce: the recipient is a key both sides hold rather than one
1788 // reached by public key, and the container is the same.
1789 let kekri = WrapDescription::new(
1790 RecipientInfoKind::Kekri,
1791 Oid::new(oid::AES_256_WRAP).unwrap(),
1792 )
1793 .with_content_encryption(Oid::new(oid::AES_256_GCM).unwrap());
1794 assert!(WrapScheme::CmsAes256Gcm.permits(&kekri));
1795
1796 // The content cipher still has to be authenticated, whichever
1797 // encapsulation carried the key to it.
1798 let kekri_cbc = WrapDescription::new(
1799 RecipientInfoKind::Kekri,
1800 Oid::new(oid::AES_256_WRAP).unwrap(),
1801 )
1802 .with_content_encryption(Oid::new(oid::AES_256_CBC).unwrap());
1803 assert!(!WrapScheme::CmsAes256Gcm.permits(&kekri_cbc));
1804
1805 // A password recipient is not one Rite writes, and admitting it would
1806 // mean a passphrase had silently stood in for a key.
1807 let pwri = WrapDescription::new(
1808 RecipientInfoKind::Pwri,
1809 Oid::new(oid::AES_256_WRAP).unwrap(),
1810 )
1811 .with_content_encryption(Oid::new(oid::AES_256_GCM).unwrap());
1812 assert!(!WrapScheme::CmsAes256Gcm.permits(&pwri));
1813 }
1814
1815 #[test]
1816 fn wrapped_key_rejects_a_scheme_its_description_contradicts() {
1817 let coherent = WrappedKey::new(WrapScheme::CmsAes256Gcm, cms_ktri_gcm(), vec![1, 2, 3]);
1818 assert!(coherent.is_ok());
1819
1820 let unauthenticated = WrapDescription::new(
1821 RecipientInfoKind::Ktri,
1822 Oid::new(oid::RSA_ENCRYPTION).unwrap(),
1823 )
1824 .with_content_encryption(Oid::new(oid::AES_256_CBC).unwrap());
1825 let mislabelled = WrappedKey::new(WrapScheme::CmsAes256Gcm, unauthenticated, vec![1, 2, 3]);
1826 assert!(
1827 mislabelled.is_err(),
1828 "bytes must not carry a label they contradict"
1829 );
1830 }
1831
1832 #[test]
1833 fn wrapped_key_recheck_survives_deserialization() {
1834 let wrapped =
1835 WrappedKey::new(WrapScheme::CmsAes256Gcm, cms_ktri_gcm(), vec![9, 9]).unwrap();
1836 let json = serde_json::to_string(&wrapped).unwrap();
1837 let back: WrappedKey = serde_json::from_str(&json).unwrap();
1838 assert_eq!(back.scheme(), WrapScheme::CmsAes256Gcm);
1839 assert_eq!(back.data(), &[9, 9]);
1840
1841 // An artifact edited to disagree with itself does not deserialize.
1842 let tampered = json.replace(oid::AES_256_GCM, oid::AES_256_CBC);
1843 assert!(serde_json::from_str::<WrappedKey>(&tampered).is_err());
1844 }
1845
1846 #[test]
1847 fn gcm_content_is_the_authenticated_case() {
1848 assert!(cms_ktri_gcm().content_is_authenticated());
1849 let cbc = WrapDescription::new(
1850 RecipientInfoKind::Ktri,
1851 Oid::new(oid::RSA_ENCRYPTION).unwrap(),
1852 )
1853 .with_content_encryption(Oid::new(oid::AES_256_CBC).unwrap());
1854 assert!(!cbc.content_is_authenticated());
1855 }
1856
1857 #[test]
1858 fn piv_slot_retired_enforces_valid_range() {
1859 // Indices 0–19 map to PIV key references 0x82–0x95.
1860 // An out-of-range index would send an invalid reference to PIV hardware.
1861 assert!(PivSlot::retired(0).is_some(), "index 0 must be valid");
1862 assert!(
1863 PivSlot::retired(19).is_some(),
1864 "index 19 must be valid (last slot 0x95)"
1865 );
1866 assert!(
1867 PivSlot::retired(20).is_none(),
1868 "index 20 must be rejected (out of range)"
1869 );
1870 assert!(
1871 PivSlot::retired(255).is_none(),
1872 "index 255 must be rejected"
1873 );
1874 }
1875
1876 #[test]
1877 fn key_policy_default_is_secure() {
1878 // The default is documented as the most secure configuration for ceremony signing keys.
1879 // A regression here (e.g., extractable=true) is a silent security downgrade.
1880 let policy = KeyPolicy::default();
1881 assert!(
1882 policy.persistent,
1883 "ceremony keys must persist across sessions"
1884 );
1885 assert!(policy.sensitive, "ceremony keys must be marked sensitive");
1886 assert!(
1887 !policy.extractable,
1888 "ceremony keys must not be extractable by default"
1889 );
1890 assert!(policy.usages.contains(KeyUsages::SIGN));
1891 assert!(policy.usages.contains(KeyUsages::VERIFY));
1892 assert!(
1893 !policy.usages.contains(KeyUsages::ENCRYPT),
1894 "signing-only default must not permit encryption"
1895 );
1896 assert!(
1897 !policy.usages.contains(KeyUsages::WRAP),
1898 "signing-only default must not permit wrapping"
1899 );
1900 }
1901}