1use serde::{Deserialize, Serialize};
16
17use crate::transcript::{Format, ValidatorSpec, compile_pattern};
18use crate::types::{ActionType, CertProfile, SharingScheme};
19use rite_sdk::{KeyAlgorithm, KeyUsages, SignAlgorithm, WrapScheme};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct ParamError {
27 pub message: String,
29}
30
31#[must_use]
40pub fn check(action: ActionType, with: &serde_json::Value) -> Vec<ParamError> {
41 match action {
42 ActionType::GenerateKey => {
43 let mut errors = named_value(with, "algorithm", |name| {
44 name.parse::<KeyAlgorithm>()
45 .map_err(|_| format!("unknown key algorithm '{name}'"))
46 });
47 errors.extend(key_usages(with));
48 errors
49 }
50 ActionType::SignData | ActionType::VerifySignature => {
51 named_value(with, "algorithm", |name| {
52 name.parse::<SignAlgorithm>()
53 .map_err(|_| format!("unknown signature algorithm '{name}'"))
54 })
55 }
56 ActionType::IssueCertificate => named_value(with, "profile", |name| {
57 name.parse::<CertProfile>()
58 .map_err(|_| format!("unknown certificate profile '{name}'"))
59 }),
60 ActionType::WrapKey => {
61 let mut errors = fingerprint(with, "expect_recipient");
62 errors.extend(scheme(with, |_| Ok(())));
63 errors
64 }
65 ActionType::EncryptData => {
69 named_value(with, "scheme", |name| match name.parse::<WrapScheme>() {
70 Ok(WrapScheme::CmsAes256Gcm) => Ok(()),
71 Ok(other) => Err(format!(
72 "encrypt_data writes {}, not {other}",
73 WrapScheme::CmsAes256Gcm
74 )),
75 Err(_) => Err(format!("unknown wrapping scheme '{name}'")),
76 })
77 }
78 ActionType::SplitSecret => {
79 let mut errors = named_value(with, "scheme", |name| {
80 name.parse::<SharingScheme>()
81 .map(|_| ())
82 .map_err(|e| format!("unknown sharing scheme '{name}'; {e}"))
83 });
84 let scheme = with
89 .get("scheme")
90 .and_then(serde_json::Value::as_str)
91 .and_then(|name| name.parse::<SharingScheme>().ok())
92 .unwrap_or(SharingScheme::RiteSssV1);
93 let max = u64::from(scheme.max_shares());
94 errors.extend(share_count(with, "threshold", max));
95 errors.extend(share_count(with, "shares", max));
96 if let (Some(threshold), Some(shares)) = (
97 with.get("threshold").and_then(serde_json::Value::as_u64),
98 with.get("shares").and_then(serde_json::Value::as_u64),
99 ) && shares < threshold
100 {
101 errors.push(ParamError {
102 message: format!(
103 "'shares' is {shares} and 'threshold' is {threshold}; fewer shares than \
104 the threshold could never reconstruct the secret"
105 ),
106 });
107 }
108 errors
109 }
110 ActionType::UnwrapKey | ActionType::ImportKey => {
111 let mut errors = key_identity(with, "expect_key");
112 errors.extend(named_value(with, "algorithm", |name| {
113 name.parse::<KeyAlgorithm>()
114 .map_err(|_| format!("unknown key algorithm '{name}'"))
115 }));
116 errors
117 }
118 ActionType::EnterValue | ActionType::EnterSecret => entry_shape(with),
119
120 ActionType::ClockCheck
121 | ActionType::Confirm
122 | ActionType::CheckValue
123 | ActionType::OralReadback
124 | ActionType::MachineInfo
125 | ActionType::ExportPublic
126 | ActionType::Attest
127 | ActionType::GatherEntropy
128 | ActionType::TpmAttest
129 | ActionType::PivReadCertificate
130 | ActionType::PivSign
131 | ActionType::YubikeyAttestSlot
132 | ActionType::DecryptData
133 | ActionType::CombineShares
134 | ActionType::GenerateCsr => Vec::new(),
135 }
136}
137
138fn share_count(with: &serde_json::Value, field: &'static str, max: u64) -> Vec<ParamError> {
144 let Some(value) = with.get(field) else {
145 return Vec::new();
146 };
147 let message = match value.as_u64() {
148 Some(n) if (2..=max).contains(&n) => return Vec::new(),
149 Some(n) => format!("'{field}' is {n}; it must be from 2 to {max}"),
150 None => format!("'{field}' must be an integer from 2 to {max}, found {value}"),
151 };
152 vec![ParamError { message }]
153}
154
155fn scheme(
161 with: &serde_json::Value,
162 accept: impl FnOnce(WrapScheme) -> Result<(), String>,
163) -> Vec<ParamError> {
164 named_value(with, "scheme", |name| {
165 let named = name
166 .parse::<WrapScheme>()
167 .map_err(|_| format!("unknown wrapping scheme '{name}'"))?;
168 accept(named)
169 })
170}
171
172fn named_value<T>(
177 with: &serde_json::Value,
178 field: &'static str,
179 parse: impl FnOnce(&str) -> Result<T, String>,
180) -> Vec<ParamError> {
181 let Some(value) = with.get(field) else {
182 return Vec::new();
183 };
184 let Some(name) = value.as_str() else {
185 return vec![ParamError {
186 message: format!("'{field}' must be a string, found {value}"),
187 }];
188 };
189 parse(name)
190 .err()
191 .map(|message| ParamError { message })
192 .into_iter()
193 .collect()
194}
195
196fn entry_shape(with: &serde_json::Value) -> Vec<ParamError> {
203 let mut errors = Vec::new();
204 let mut length = |field: &'static str| -> Option<usize> {
205 let value = with.get(field)?;
206 match value.as_u64().and_then(|n| usize::try_from(n).ok()) {
207 Some(n) if n > 0 => Some(n),
208 _ => {
209 errors.push(ParamError {
210 message: format!("'{field}' must be a positive integer, found {value}"),
211 });
212 None
213 }
214 }
215 };
216 let mut shape = EntryShape {
217 length: length("length"),
218 min_length: length("min_length"),
219 max_length: length("max_length"),
220 format: None,
221 };
222 match with.get("format").map(FormatSpec::from_json) {
223 None => {}
224 Some(Ok(format)) => shape.format = Some(format),
225 Some(Err(message)) => errors.push(ParamError { message }),
226 }
227 if let Err(message) = shape.validator() {
228 errors.push(ParamError { message });
229 }
230 errors
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(untagged)]
239pub enum FormatSpec {
240 Named(Format),
242 Pattern {
244 pattern: String,
246 },
247}
248
249impl FormatSpec {
250 pub fn from_json(value: &serde_json::Value) -> Result<Self, String> {
256 if let Some(name) = value.as_str() {
257 return name.parse().map(FormatSpec::Named);
258 }
259 match value.get("pattern").and_then(|p| p.as_str()) {
260 Some(pattern) if value.as_object().is_some_and(|m| m.len() == 1) => {
261 Ok(FormatSpec::Pattern {
262 pattern: pattern.to_string(),
263 })
264 }
265 _ => Err(format!(
266 "'format' must name a format (text, digits, alphanumeric, hex, base64) or be \
267 {{ pattern: \"...\" }}, found {value}"
268 )),
269 }
270 }
271}
272
273#[derive(Debug, Clone, Default, PartialEq, Eq)]
279pub struct EntryShape {
280 pub format: Option<FormatSpec>,
282 pub length: Option<usize>,
284 pub min_length: Option<usize>,
286 pub max_length: Option<usize>,
288}
289
290impl EntryShape {
291 pub fn validator(&self) -> Result<ValidatorSpec, String> {
303 let bounded =
304 self.length.is_some() || self.min_length.is_some() || self.max_length.is_some();
305 let format = match &self.format {
306 Some(FormatSpec::Pattern { pattern }) => {
307 if bounded {
308 return Err("a pattern says the whole shape of the value, so 'length', \
309 'min_length' and 'max_length' cannot be given beside it"
310 .to_string());
311 }
312 compile_pattern(pattern)?;
313 return Ok(ValidatorSpec::Regex(pattern.clone()));
314 }
315 Some(FormatSpec::Named(format)) => Some(*format),
316 None => None,
317 };
318 let (min_length, max_length) = match (self.length, self.min_length, self.max_length) {
319 (Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
320 return Err(
321 "'length' is exact, so 'min_length' and 'max_length' cannot be given \
322 beside it"
323 .to_string(),
324 );
325 }
326 (Some(length), None, None) => (Some(length), Some(length)),
327 (None, min, max) => (min, max),
328 };
329 if let (Some(min), Some(max)) = (min_length, max_length)
330 && min > max
331 {
332 return Err(format!(
333 "'min_length' is {min} and 'max_length' is {max}, so no value fits"
334 ));
335 }
336 match format {
337 None if !bounded => Ok(ValidatorSpec::NonEmpty),
338 format => Ok(ValidatorSpec::Format {
339 format: format.unwrap_or(Format::Text),
340 min_length,
341 max_length,
342 }),
343 }
344 }
345}
346
347fn key_usages(with: &serde_json::Value) -> Vec<ParamError> {
353 const FIELD: &str = "policy.usages";
354 let Some(usages) = with.get("policy").and_then(|p| p.get("usages")) else {
355 return Vec::new();
356 };
357 let Some(names) = usages.as_array() else {
358 return vec![ParamError {
359 message: format!("'{FIELD}' must be a list, found {usages}"),
360 }];
361 };
362 names
363 .iter()
364 .filter_map(|name| {
365 let known = name
366 .as_str()
367 .is_some_and(|name| KeyUsages::usage_named(name).is_some());
368 (!known).then(|| ParamError {
369 message: format!(
370 "unknown key usage {name} under '{FIELD}'. Supported: {}",
371 KeyUsages::all_names().join(", ")
372 ),
373 })
374 })
375 .collect()
376}
377
378fn fingerprint(with: &serde_json::Value, field: &'static str) -> Vec<ParamError> {
385 named_value(with, field, |value| {
386 let hex = value.strip_prefix("sha256:").unwrap_or_default();
387 if hex.len() == 64 && hex.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
388 Ok(())
389 } else {
390 Err(format!(
391 "'{field}' must be a fingerprint of the form \
392 'sha256:<64 lowercase hex digits>', found '{value}'"
393 ))
394 }
395 })
396}
397
398fn key_identity(with: &serde_json::Value, field: &'static str) -> Vec<ParamError> {
406 named_value(with, field, |value| {
407 let lower_hex = |s: &str, len: usize| {
408 s.len() == len && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
409 };
410 let matches_form = match value.split_once(':') {
411 Some(("sha256", hex)) => lower_hex(hex, 64),
412 Some(("cmac-aes", hex)) => lower_hex(hex, 6),
413 _ => false,
414 };
415 if matches_form {
416 Ok(())
417 } else {
418 Err(format!(
419 "'{field}' must name the key either as 'sha256:<64 lowercase hex digits>', \
420 the fingerprint of a public key, or as 'cmac-aes:<6 lowercase hex digits>', \
421 the check value of a symmetric key. Found '{value}'"
422 ))
423 }
424 })
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430 use serde_json::json;
431
432 fn sole(action: ActionType, with: &serde_json::Value) -> String {
433 let errors = check(action, with);
434 let [error] = errors.as_slice() else {
435 panic!("expected exactly one error, got {errors:?}");
436 };
437 error.message.clone()
438 }
439
440 #[test]
441 fn rejects_a_value_outside_the_vocabulary() {
442 assert!(
443 sole(ActionType::GenerateKey, &json!({"algorithm": "RSA-9999"}))
444 .contains("unknown key algorithm")
445 );
446 assert!(
447 sole(ActionType::SignData, &json!({"algorithm": "RSA-PSS-SHA1"}))
448 .contains("unknown signature algorithm")
449 );
450 assert!(
451 sole(
452 ActionType::IssueCertificate,
453 &json!({"profile": "wildcard"})
454 )
455 .contains("unknown certificate profile")
456 );
457 assert!(
458 sole(
459 ActionType::WrapKey,
460 &json!({"expect_recipient": "deadbeef"})
461 )
462 .contains("sha256:<64 lowercase hex digits>")
463 );
464 }
465
466 #[test]
470 fn rejects_a_fingerprint_that_is_not_lowercase() {
471 let upper = format!("sha256:{}", "AB".repeat(32));
472 let message = sole(ActionType::WrapKey, &json!({ "expect_recipient": upper }));
473 assert!(message.contains("lowercase"), "{message}");
474
475 let lower = format!("sha256:{}", "ab".repeat(32));
476 assert!(
477 check(ActionType::WrapKey, &json!({ "expect_recipient": lower })).is_empty(),
478 "the shape the runtime produces is the shape that passes"
479 );
480 }
481
482 #[test]
485 fn rejects_a_scheme_encrypt_data_does_not_write() {
486 let message = sole(ActionType::EncryptData, &json!({"scheme": "AES-256-KW"}));
487 assert!(
488 message.contains("encrypt_data writes CMS-AES-256-GCM"),
489 "{message}"
490 );
491
492 let message = sole(ActionType::EncryptData, &json!({"scheme": "CMS-RSA-CBC"}));
493 assert!(message.contains("unknown wrapping scheme"), "{message}");
494
495 assert!(
496 check(
497 ActionType::EncryptData,
498 &json!({"scheme": "CMS-AES-256-GCM"})
499 )
500 .is_empty()
501 );
502 assert!(
503 check(ActionType::EncryptData, &json!({})).is_empty(),
504 "deferred"
505 );
506
507 assert!(check(ActionType::WrapKey, &json!({"scheme": "AES-256-KW"})).is_empty());
509 }
510
511 #[test]
514 fn rejects_a_wrapping_scheme_outside_the_vocabulary() {
515 let message = sole(ActionType::WrapKey, &json!({"scheme": "CMS-RSA-CBC"}));
516 assert!(message.contains("unknown wrapping scheme"), "{message}");
517
518 assert!(
519 check(
520 ActionType::WrapKey,
521 &json!({"scheme": "RSA-AES-KEY-WRAP-SHA256"})
522 )
523 .is_empty()
524 );
525 assert!(
526 check(ActionType::WrapKey, &json!({})).is_empty(),
527 "deferred"
528 );
529 }
530
531 #[test]
532 fn accepts_the_vocabulary() {
533 for (action, with) in [
534 (ActionType::GenerateKey, json!({"algorithm": "RSA-4096"})),
535 (ActionType::SignData, json!({"algorithm": "RSA-PSS-SHA256"})),
536 (ActionType::IssueCertificate, json!({"profile": "root_ca"})),
537 (
538 ActionType::UnwrapKey,
539 json!({"expect_key": format!("sha256:{}", "ab".repeat(32))}),
540 ),
541 (
542 ActionType::UnwrapKey,
543 json!({"algorithm": "AES-256", "expect_key": "cmac-aes:763cbc"}),
544 ),
545 ] {
546 assert!(check(action, &with).is_empty(), "{action} rejected {with}");
547 }
548 }
549
550 #[test]
553 fn expect_key_takes_a_fingerprint_or_a_check_value() {
554 let message = sole(
555 ActionType::UnwrapKey,
556 &json!({"expect_key": "cmac-aes:763CBC"}),
557 );
558 assert!(message.contains("lowercase"), "{message}");
559
560 let message = sole(
563 ActionType::UnwrapKey,
564 &json!({"expect_key": format!("cmac-aes:{}", "ab".repeat(32))}),
565 );
566 assert!(message.contains("6 lowercase hex digits"), "{message}");
567
568 let message = sole(ActionType::UnwrapKey, &json!({"expect_key": "763cbc"}));
569 assert!(
570 message.contains("sha256:") && message.contains("cmac-aes:"),
571 "{message}"
572 );
573 }
574
575 #[test]
579 fn rejects_an_unwrap_algorithm_outside_the_vocabulary() {
580 let message = sole(ActionType::UnwrapKey, &json!({"algorithm": "AES-192"}));
581 assert!(message.contains("unknown key algorithm"), "{message}");
582 }
583
584 #[test]
585 fn an_absent_field_is_deferred_not_missing() {
586 assert!(check(ActionType::GenerateKey, &json!({})).is_empty());
589 assert!(check(ActionType::IssueCertificate, &json!({})).is_empty());
590 }
591
592 #[test]
593 fn rejects_an_unknown_key_usage() {
594 let message = sole(
595 ActionType::GenerateKey,
596 &json!({"policy": {"usages": ["sign", "key_cert_sign"]}}),
597 );
598 assert!(message.contains("unknown key usage"), "{message}");
599 assert!(
600 message.contains("wrap"),
601 "the message should list what is available: {message}"
602 );
603 }
604
605 #[test]
606 fn accepts_the_pkcs11_usages() {
607 let with = json!({"policy": {"usages": ["sign", "verify", "wrap", "unwrap", "derive"]}});
608 assert!(check(ActionType::GenerateKey, &with).is_empty());
609 }
610
611 #[test]
612 fn a_non_string_value_is_rejected_whatever_it_would_have_meant() {
613 assert!(
614 sole(ActionType::GenerateKey, &json!({"algorithm": 4096})).contains("must be a string")
615 );
616 }
617
618 #[test]
619 fn split_secret_counts_are_bounded_by_the_scheme() {
620 assert!(
621 check(
622 ActionType::SplitSecret,
623 &json!({"threshold": 2, "shares": 100})
624 )
625 .is_empty()
626 );
627 assert!(
628 sole(
629 ActionType::SplitSecret,
630 &json!({"threshold": 2, "shares": 101})
631 )
632 .contains("from 2 to 100")
633 );
634 assert!(
635 sole(
636 ActionType::SplitSecret,
637 &json!({"threshold": 1, "shares": 3})
638 )
639 .contains("from 2 to 100")
640 );
641 assert!(
642 sole(
643 ActionType::SplitSecret,
644 &json!({"threshold": 4, "shares": 3})
645 )
646 .contains("fewer shares than the threshold")
647 );
648 assert!(
649 sole(
650 ActionType::SplitSecret,
651 &json!({"threshold": "two", "shares": 3})
652 )
653 .contains("must be an integer")
654 );
655 assert!(
656 sole(
657 ActionType::SplitSecret,
658 &json!({"scheme": "sss/v9", "threshold": 2, "shares": 3})
659 )
660 .contains("unknown sharing scheme")
661 );
662 assert!(check(ActionType::SplitSecret, &json!({"shares": 3})).is_empty());
665 }
666
667 #[test]
669 fn an_entry_shape_becomes_one_rule() {
670 let shape = EntryShape {
671 format: Some(FormatSpec::Named(Format::Digits)),
672 length: Some(6),
673 ..EntryShape::default()
674 };
675 assert!(matches!(
676 shape.validator(),
677 Ok(ValidatorSpec::Format {
678 format: Format::Digits,
679 min_length: Some(6),
680 max_length: Some(6),
681 })
682 ));
683
684 let shape = EntryShape {
686 max_length: Some(8),
687 ..EntryShape::default()
688 };
689 assert!(matches!(
690 shape.validator(),
691 Ok(ValidatorSpec::Format {
692 format: Format::Text,
693 min_length: None,
694 max_length: Some(8),
695 })
696 ));
697
698 assert!(matches!(
699 EntryShape::default().validator(),
700 Ok(ValidatorSpec::NonEmpty)
701 ));
702 assert!(matches!(
703 EntryShape {
704 format: Some(FormatSpec::Pattern {
705 pattern: "[a-z]+".to_string()
706 }),
707 ..EntryShape::default()
708 }
709 .validator(),
710 Ok(ValidatorSpec::Regex(_))
711 ));
712 }
713
714 #[test]
715 fn rejects_an_entry_shape_that_is_two_rules() {
716 assert!(
717 sole(
718 ActionType::EnterValue,
719 &json!({"format": {"pattern": "[0-9]+"}, "length": 6})
720 )
721 .contains("pattern")
722 );
723 assert!(
724 sole(
725 ActionType::EnterSecret,
726 &json!({"length": 6, "min_length": 4})
727 )
728 .contains("'length' is exact")
729 );
730 assert!(
731 sole(
732 ActionType::EnterSecret,
733 &json!({"min_length": 8, "max_length": 6})
734 )
735 .contains("no value fits")
736 );
737 }
738
739 #[test]
740 fn rejects_an_entry_shape_outside_the_vocabulary() {
741 assert!(
742 sole(ActionType::EnterSecret, &json!({"format": "emoji"})).contains("unknown format")
743 );
744 assert!(
745 sole(
746 ActionType::EnterSecret,
747 &json!({"format": {"pattern": "[0-9"}})
748 )
749 .contains("invalid pattern")
750 );
751 assert!(
752 sole(
753 ActionType::EnterSecret,
754 &json!({"format": {"regex": "[0-9]+"}})
755 )
756 .contains("'format' must name a format")
757 );
758 assert!(sole(ActionType::EnterSecret, &json!({"length": 0})).contains("positive integer"));
759 assert!(
760 sole(ActionType::EnterSecret, &json!({"length": "six"})).contains("positive integer")
761 );
762 assert!(check(ActionType::EnterSecret, &json!({"message": "PIN"})).is_empty());
763 assert!(
764 check(
765 ActionType::EnterSecret,
766 &json!({"message": "Key", "format": "hex", "length": 32})
767 )
768 .is_empty()
769 );
770 }
771}