1use serde::{Deserialize, Serialize};
16
17use crate::display::RevealFormat;
18use crate::transcript::{Format, ValidatorSpec, compile_pattern};
19use crate::types::{ActionType, CertProfile, SharingScheme};
20use rite_sdk::{KeyAlgorithm, KeyUsages, SignAlgorithm, WrapScheme};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ParamError {
28 pub message: String,
30}
31
32#[must_use]
41pub fn check(action: ActionType, with: &serde_json::Value) -> Vec<ParamError> {
42 match action {
43 ActionType::GenerateKey => {
44 let mut errors = named_value(with, "algorithm", |name| {
45 name.parse::<KeyAlgorithm>()
46 .map_err(|_| format!("unknown key algorithm '{name}'"))
47 });
48 errors.extend(key_usages(with));
49 errors
50 }
51 ActionType::SignData | ActionType::VerifySignature => {
52 named_value(with, "algorithm", |name| {
53 name.parse::<SignAlgorithm>()
54 .map_err(|_| format!("unknown signature algorithm '{name}'"))
55 })
56 }
57 ActionType::IssueCertificate => named_value(with, "profile", |name| {
58 name.parse::<CertProfile>()
59 .map_err(|_| format!("unknown certificate profile '{name}'"))
60 }),
61 ActionType::WrapKey => {
62 let mut errors = fingerprint(with, "expect_recipient");
63 errors.extend(scheme(with, |_| Ok(())));
64 errors
65 }
66 ActionType::EncryptData => {
70 named_value(with, "scheme", |name| match name.parse::<WrapScheme>() {
71 Ok(WrapScheme::CmsAes256Gcm) => Ok(()),
72 Ok(other) => Err(format!(
73 "encrypt_data writes {}, not {other}",
74 WrapScheme::CmsAes256Gcm
75 )),
76 Err(_) => Err(format!("unknown wrapping scheme '{name}'")),
77 })
78 }
79 ActionType::SplitSecret => {
80 let mut errors = named_value(with, "scheme", |name| {
81 name.parse::<SharingScheme>()
82 .map(|_| ())
83 .map_err(|e| format!("unknown sharing scheme '{name}'; {e}"))
84 });
85 let scheme = with
90 .get("scheme")
91 .and_then(serde_json::Value::as_str)
92 .and_then(|name| name.parse::<SharingScheme>().ok())
93 .unwrap_or(SharingScheme::RiteSssV1);
94 let max = u64::from(scheme.max_shares());
95 errors.extend(share_count(with, "threshold", max));
96 errors.extend(share_count(with, "shares", max));
97 if let (Some(threshold), Some(shares)) = (
98 with.get("threshold").and_then(serde_json::Value::as_u64),
99 with.get("shares").and_then(serde_json::Value::as_u64),
100 ) && shares < threshold
101 {
102 errors.push(ParamError {
103 message: format!(
104 "'shares' is {shares} and 'threshold' is {threshold}; fewer shares than \
105 the threshold could never reconstruct the secret"
106 ),
107 });
108 }
109 errors
110 }
111 ActionType::Reveal => reveal(with),
112 ActionType::EnterShare => enter_share(with),
113 ActionType::UnwrapKey | ActionType::ImportKey => {
114 let mut errors = key_identity(with, "expect_key");
115 errors.extend(named_value(with, "algorithm", |name| {
116 name.parse::<KeyAlgorithm>()
117 .map_err(|_| format!("unknown key algorithm '{name}'"))
118 }));
119 errors
120 }
121 ActionType::EnterSecret => entry_shape(with),
122 ActionType::EnterValue => enter_value(with),
123
124 ActionType::ClockCheck
125 | ActionType::Confirm
126 | ActionType::CheckValue
127 | ActionType::OralReadback
128 | ActionType::MachineInfo
129 | ActionType::ExportPublic
130 | ActionType::Attest
131 | ActionType::GatherEntropy
132 | ActionType::TpmAttest
133 | ActionType::PivReadCertificate
134 | ActionType::PivSign
135 | ActionType::YubikeyAttestSlot
136 | ActionType::DecryptData
137 | ActionType::CombineShares
138 | ActionType::GenerateCsr => Vec::new(),
139 }
140}
141
142fn enter_value(with: &serde_json::Value) -> Vec<ParamError> {
145 let mut errors = entry_shape(with);
146 let rows = match with.get("format").map(FormatSpec::from_json) {
147 Some(Ok(FormatSpec::Named(format))) => format.rows(),
148 _ => None,
149 };
150 if let Some(rows) = rows {
151 errors.push(ParamError {
152 message: format!(
153 "'{rows}' is for a value written down, which is a secret: use enter_secret"
154 ),
155 });
156 }
157 errors
158}
159
160fn enter_share(with: &serde_json::Value) -> Vec<ParamError> {
163 let mut errors = reveal(with);
164 let format = shown_format(with);
165 if format.is_words() {
166 errors.push(ParamError {
167 message: format!(
168 "a share is written in paper32 or hex; '{format}' is for the secret the shares \
169 combine into"
170 ),
171 });
172 }
173 let header = SharingScheme::RiteSssV1.header_len();
175 if let Some(n) = with.get("length").and_then(serde_json::Value::as_u64)
176 && usize::try_from(n).is_ok_and(|n| (1..=header).contains(&n))
177 {
178 errors.push(ParamError {
179 message: format!(
180 "'length' is {n}; a share is its secret plus {header} header bytes, so it is \
181 longer than {header}"
182 ),
183 });
184 }
185 errors
186}
187
188fn shown_format(with: &serde_json::Value) -> RevealFormat {
191 with.get("format")
192 .and_then(serde_json::Value::as_str)
193 .and_then(|name| name.parse().ok())
194 .unwrap_or(RevealFormat::Paper32)
195}
196
197fn reveal(with: &serde_json::Value) -> Vec<ParamError> {
200 let mut errors = named_value(with, "format", |name| {
201 name.parse::<RevealFormat>()
202 .map(|_| ())
203 .map_err(|e| e.to_string())
204 });
205 let format = shown_format(with);
206 let Some(value) = with.get("length") else {
207 return errors;
208 };
209 let message = match value.as_u64().and_then(|n| usize::try_from(n).ok()) {
210 None | Some(0) => Some(format!(
211 "'length' must be a positive integer of {}, found {value}",
212 format.unit()
213 )),
214 Some(n) if format.is_words() && !crate::bip39::is_phrase_len(n) => Some(format!(
215 "'length' counts words for '{format}', and a phrase has {}, found {value}",
216 crate::bip39::PHRASE_LENGTHS
217 )),
218 Some(_) => None,
219 };
220 errors.extend(message.map(|message| ParamError { message }));
221 errors
222}
223
224fn share_count(with: &serde_json::Value, field: &'static str, max: u64) -> Vec<ParamError> {
230 let Some(value) = with.get(field) else {
231 return Vec::new();
232 };
233 let message = match value.as_u64() {
234 Some(n) if (2..=max).contains(&n) => return Vec::new(),
235 Some(n) => format!("'{field}' is {n}; it must be from 2 to {max}"),
236 None => format!("'{field}' must be an integer from 2 to {max}, found {value}"),
237 };
238 vec![ParamError { message }]
239}
240
241fn scheme(
247 with: &serde_json::Value,
248 accept: impl FnOnce(WrapScheme) -> Result<(), String>,
249) -> Vec<ParamError> {
250 named_value(with, "scheme", |name| {
251 let named = name
252 .parse::<WrapScheme>()
253 .map_err(|_| format!("unknown wrapping scheme '{name}'"))?;
254 accept(named)
255 })
256}
257
258fn named_value<T>(
263 with: &serde_json::Value,
264 field: &'static str,
265 parse: impl FnOnce(&str) -> Result<T, String>,
266) -> Vec<ParamError> {
267 let Some(value) = with.get(field) else {
268 return Vec::new();
269 };
270 let Some(name) = value.as_str() else {
271 return vec![ParamError {
272 message: format!("'{field}' must be a string, found {value}"),
273 }];
274 };
275 parse(name)
276 .err()
277 .map(|message| ParamError { message })
278 .into_iter()
279 .collect()
280}
281
282fn entry_shape(with: &serde_json::Value) -> Vec<ParamError> {
289 let mut errors = Vec::new();
290 let mut length = |field: &'static str| -> Option<usize> {
291 let value = with.get(field)?;
292 match value.as_u64().and_then(|n| usize::try_from(n).ok()) {
293 Some(n) if n > 0 => Some(n),
294 _ => {
295 errors.push(ParamError {
296 message: format!("'{field}' must be a positive integer, found {value}"),
297 });
298 None
299 }
300 }
301 };
302 let mut shape = EntryShape {
303 length: length("length"),
304 min_length: length("min_length"),
305 max_length: length("max_length"),
306 format: None,
307 };
308 match with.get("format").map(FormatSpec::from_json) {
309 None => {}
310 Some(Ok(format)) => shape.format = Some(format),
311 Some(Err(message)) => errors.push(ParamError { message }),
312 }
313 if let Err(message) = shape.validator() {
314 errors.push(ParamError { message });
315 }
316 errors
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
324#[serde(untagged)]
325pub enum FormatSpec {
326 Named(Format),
328 Pattern {
330 pattern: String,
332 },
333}
334
335impl FormatSpec {
336 pub fn from_json(value: &serde_json::Value) -> Result<Self, String> {
342 if let Some(name) = value.as_str() {
343 return name.parse().map(FormatSpec::Named);
344 }
345 match value.get("pattern").and_then(|p| p.as_str()) {
346 Some(pattern) if value.as_object().is_some_and(|m| m.len() == 1) => {
347 Ok(FormatSpec::Pattern {
348 pattern: pattern.to_string(),
349 })
350 }
351 _ => Err(format!(
352 "'format' must name a format (text, digits, alphanumeric, hex, base64, \
353 paper32, bip39) or be \
354 {{ pattern: \"...\" }}, found {value}"
355 )),
356 }
357 }
358}
359
360#[derive(Debug, Clone, Default, PartialEq, Eq)]
366pub struct EntryShape {
367 pub format: Option<FormatSpec>,
369 pub length: Option<usize>,
371 pub min_length: Option<usize>,
373 pub max_length: Option<usize>,
375}
376
377impl EntryShape {
378 pub fn validator(&self) -> Result<ValidatorSpec, String> {
390 let bounded =
391 self.length.is_some() || self.min_length.is_some() || self.max_length.is_some();
392 let format = match &self.format {
393 Some(FormatSpec::Pattern { pattern }) => {
394 if bounded {
395 return Err("a pattern says the whole shape of the value, so 'length', \
396 'min_length' and 'max_length' cannot be given beside it"
397 .to_string());
398 }
399 compile_pattern(pattern)?;
400 return Ok(ValidatorSpec::Regex(pattern.clone()));
401 }
402 Some(FormatSpec::Named(format)) => Some(*format),
403 None => None,
404 };
405 let (min_length, max_length) = match (self.length, self.min_length, self.max_length) {
406 (Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
407 return Err(
408 "'length' is exact, so 'min_length' and 'max_length' cannot be given \
409 beside it"
410 .to_string(),
411 );
412 }
413 (Some(length), None, None) => (Some(length), Some(length)),
414 (None, min, max) => (min, max),
415 };
416 if let (Some(min), Some(max)) = (min_length, max_length)
417 && min > max
418 {
419 return Err(format!(
420 "'min_length' is {min} and 'max_length' is {max}, so no value fits"
421 ));
422 }
423 if format == Some(Format::Bip39)
425 && !crate::bip39::phrase_lengths().any(|n| {
426 min_length.is_none_or(|min| n >= min) && max_length.is_none_or(|max| n <= max)
427 })
428 {
429 return Err(format!(
430 "a BIP-39 phrase has {} words, and the length given holds none of them",
431 crate::bip39::PHRASE_LENGTHS
432 ));
433 }
434 match format {
435 None if !bounded => Ok(ValidatorSpec::NonEmpty),
436 format => Ok(ValidatorSpec::Format {
437 format: format.unwrap_or(Format::Text),
438 min_length,
439 max_length,
440 }),
441 }
442 }
443}
444
445fn key_usages(with: &serde_json::Value) -> Vec<ParamError> {
451 const FIELD: &str = "policy.usages";
452 let Some(usages) = with.get("policy").and_then(|p| p.get("usages")) else {
453 return Vec::new();
454 };
455 let Some(names) = usages.as_array() else {
456 return vec![ParamError {
457 message: format!("'{FIELD}' must be a list, found {usages}"),
458 }];
459 };
460 names
461 .iter()
462 .filter_map(|name| {
463 let known = name
464 .as_str()
465 .is_some_and(|name| KeyUsages::usage_named(name).is_some());
466 (!known).then(|| ParamError {
467 message: format!(
468 "unknown key usage {name} under '{FIELD}'. Supported: {}",
469 KeyUsages::all_names().join(", ")
470 ),
471 })
472 })
473 .collect()
474}
475
476fn fingerprint(with: &serde_json::Value, field: &'static str) -> Vec<ParamError> {
483 named_value(with, field, |value| {
484 let hex = value.strip_prefix("sha256:").unwrap_or_default();
485 if hex.len() == 64 && hex.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
486 Ok(())
487 } else {
488 Err(format!(
489 "'{field}' must be a fingerprint of the form \
490 'sha256:<64 lowercase hex digits>', found '{value}'"
491 ))
492 }
493 })
494}
495
496fn key_identity(with: &serde_json::Value, field: &'static str) -> Vec<ParamError> {
504 named_value(with, field, |value| {
505 let lower_hex = |s: &str, len: usize| {
506 s.len() == len && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
507 };
508 let matches_form = match value.split_once(':') {
509 Some(("sha256", hex)) => lower_hex(hex, 64),
510 Some(("cmac-aes", hex)) => lower_hex(hex, 6),
511 _ => false,
512 };
513 if matches_form {
514 Ok(())
515 } else {
516 Err(format!(
517 "'{field}' must name the key either as 'sha256:<64 lowercase hex digits>', \
518 the fingerprint of a public key, or as 'cmac-aes:<6 lowercase hex digits>', \
519 the check value of a symmetric key. Found '{value}'"
520 ))
521 }
522 })
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528 use serde_json::json;
529
530 fn sole(action: ActionType, with: &serde_json::Value) -> String {
531 let errors = check(action, with);
532 let [error] = errors.as_slice() else {
533 panic!("expected exactly one error, got {errors:?}");
534 };
535 error.message.clone()
536 }
537
538 #[test]
539 fn rejects_a_value_outside_the_vocabulary() {
540 assert!(
541 sole(ActionType::GenerateKey, &json!({"algorithm": "RSA-9999"}))
542 .contains("unknown key algorithm")
543 );
544 assert!(
545 sole(ActionType::SignData, &json!({"algorithm": "RSA-PSS-SHA1"}))
546 .contains("unknown signature algorithm")
547 );
548 assert!(
549 sole(
550 ActionType::IssueCertificate,
551 &json!({"profile": "wildcard"})
552 )
553 .contains("unknown certificate profile")
554 );
555 assert!(
556 sole(
557 ActionType::WrapKey,
558 &json!({"expect_recipient": "deadbeef"})
559 )
560 .contains("sha256:<64 lowercase hex digits>")
561 );
562 }
563
564 #[test]
568 fn rejects_a_fingerprint_that_is_not_lowercase() {
569 let upper = format!("sha256:{}", "AB".repeat(32));
570 let message = sole(ActionType::WrapKey, &json!({ "expect_recipient": upper }));
571 assert!(message.contains("lowercase"), "{message}");
572
573 let lower = format!("sha256:{}", "ab".repeat(32));
574 assert!(
575 check(ActionType::WrapKey, &json!({ "expect_recipient": lower })).is_empty(),
576 "the shape the runtime produces is the shape that passes"
577 );
578 }
579
580 #[test]
583 fn rejects_a_scheme_encrypt_data_does_not_write() {
584 let message = sole(ActionType::EncryptData, &json!({"scheme": "AES-256-KW"}));
585 assert!(
586 message.contains("encrypt_data writes CMS-AES-256-GCM"),
587 "{message}"
588 );
589
590 let message = sole(ActionType::EncryptData, &json!({"scheme": "CMS-RSA-CBC"}));
591 assert!(message.contains("unknown wrapping scheme"), "{message}");
592
593 assert!(
594 check(
595 ActionType::EncryptData,
596 &json!({"scheme": "CMS-AES-256-GCM"})
597 )
598 .is_empty()
599 );
600 assert!(
601 check(ActionType::EncryptData, &json!({})).is_empty(),
602 "deferred"
603 );
604
605 assert!(check(ActionType::WrapKey, &json!({"scheme": "AES-256-KW"})).is_empty());
607 }
608
609 #[test]
612 fn rejects_a_wrapping_scheme_outside_the_vocabulary() {
613 let message = sole(ActionType::WrapKey, &json!({"scheme": "CMS-RSA-CBC"}));
614 assert!(message.contains("unknown wrapping scheme"), "{message}");
615
616 assert!(
617 check(
618 ActionType::WrapKey,
619 &json!({"scheme": "RSA-AES-KEY-WRAP-SHA256"})
620 )
621 .is_empty()
622 );
623 assert!(
624 check(ActionType::WrapKey, &json!({})).is_empty(),
625 "deferred"
626 );
627 }
628
629 #[test]
630 fn accepts_the_vocabulary() {
631 for (action, with) in [
632 (ActionType::GenerateKey, json!({"algorithm": "RSA-4096"})),
633 (ActionType::SignData, json!({"algorithm": "RSA-PSS-SHA256"})),
634 (ActionType::IssueCertificate, json!({"profile": "root_ca"})),
635 (
636 ActionType::UnwrapKey,
637 json!({"expect_key": format!("sha256:{}", "ab".repeat(32))}),
638 ),
639 (
640 ActionType::UnwrapKey,
641 json!({"algorithm": "AES-256", "expect_key": "cmac-aes:763cbc"}),
642 ),
643 ] {
644 assert!(check(action, &with).is_empty(), "{action} rejected {with}");
645 }
646 }
647
648 #[test]
651 fn expect_key_takes_a_fingerprint_or_a_check_value() {
652 let message = sole(
653 ActionType::UnwrapKey,
654 &json!({"expect_key": "cmac-aes:763CBC"}),
655 );
656 assert!(message.contains("lowercase"), "{message}");
657
658 let message = sole(
661 ActionType::UnwrapKey,
662 &json!({"expect_key": format!("cmac-aes:{}", "ab".repeat(32))}),
663 );
664 assert!(message.contains("6 lowercase hex digits"), "{message}");
665
666 let message = sole(ActionType::UnwrapKey, &json!({"expect_key": "763cbc"}));
667 assert!(
668 message.contains("sha256:") && message.contains("cmac-aes:"),
669 "{message}"
670 );
671 }
672
673 #[test]
677 fn rejects_an_unwrap_algorithm_outside_the_vocabulary() {
678 let message = sole(ActionType::UnwrapKey, &json!({"algorithm": "AES-192"}));
679 assert!(message.contains("unknown key algorithm"), "{message}");
680 }
681
682 #[test]
683 fn an_absent_field_is_deferred_not_missing() {
684 assert!(check(ActionType::GenerateKey, &json!({})).is_empty());
687 assert!(check(ActionType::IssueCertificate, &json!({})).is_empty());
688 }
689
690 #[test]
691 fn rejects_an_unknown_key_usage() {
692 let message = sole(
693 ActionType::GenerateKey,
694 &json!({"policy": {"usages": ["sign", "key_cert_sign"]}}),
695 );
696 assert!(message.contains("unknown key usage"), "{message}");
697 assert!(
698 message.contains("wrap"),
699 "the message should list what is available: {message}"
700 );
701 }
702
703 #[test]
704 fn accepts_the_pkcs11_usages() {
705 let with = json!({"policy": {"usages": ["sign", "verify", "wrap", "unwrap", "derive"]}});
706 assert!(check(ActionType::GenerateKey, &with).is_empty());
707 }
708
709 #[test]
710 fn a_non_string_value_is_rejected_whatever_it_would_have_meant() {
711 assert!(
712 sole(ActionType::GenerateKey, &json!({"algorithm": 4096})).contains("must be a string")
713 );
714 }
715
716 #[test]
717 fn reveal_checks_its_format_and_length() {
718 assert!(
719 check(
720 ActionType::Reveal,
721 &json!({"message": "Write this down", "format": "paper32", "length": 32})
722 )
723 .is_empty()
724 );
725 assert!(sole(ActionType::Reveal, &json!({"format": "words"})).contains("not a format"));
726 assert!(sole(ActionType::Reveal, &json!({"length": 0})).contains("positive integer"));
727 assert!(
728 check(
729 ActionType::Reveal,
730 &json!({"format": "bip39", "length": 24})
731 )
732 .is_empty()
733 );
734 assert!(
735 sole(
736 ActionType::Reveal,
737 &json!({"format": "bip39", "length": 32})
738 )
739 .contains("counts words")
740 );
741 assert!(
742 sole(ActionType::EnterShare, &json!({"format": "bip39"}))
743 .contains("a share is written")
744 );
745 assert!(check(ActionType::EnterShare, &json!({"length": 35})).is_empty());
746 assert!(sole(ActionType::EnterShare, &json!({"length": 3})).contains("longer than 3"));
747 assert!(sole(ActionType::EnterShare, &json!({"length": 0})).contains("positive integer"));
748 }
749
750 #[test]
751 fn split_secret_counts_are_bounded_by_the_scheme() {
752 assert!(
753 check(
754 ActionType::SplitSecret,
755 &json!({"threshold": 2, "shares": 100})
756 )
757 .is_empty()
758 );
759 assert!(
760 sole(
761 ActionType::SplitSecret,
762 &json!({"threshold": 2, "shares": 101})
763 )
764 .contains("from 2 to 100")
765 );
766 assert!(
767 sole(
768 ActionType::SplitSecret,
769 &json!({"threshold": 1, "shares": 3})
770 )
771 .contains("from 2 to 100")
772 );
773 assert!(
774 sole(
775 ActionType::SplitSecret,
776 &json!({"threshold": 4, "shares": 3})
777 )
778 .contains("fewer shares than the threshold")
779 );
780 assert!(
781 sole(
782 ActionType::SplitSecret,
783 &json!({"threshold": "two", "shares": 3})
784 )
785 .contains("must be an integer")
786 );
787 assert!(
788 sole(
789 ActionType::SplitSecret,
790 &json!({"scheme": "sss/v9", "threshold": 2, "shares": 3})
791 )
792 .contains("unknown sharing scheme")
793 );
794 assert!(check(ActionType::SplitSecret, &json!({"shares": 3})).is_empty());
797 }
798
799 #[test]
801 fn an_entry_shape_becomes_one_rule() {
802 let shape = EntryShape {
803 format: Some(FormatSpec::Named(Format::Digits)),
804 length: Some(6),
805 ..EntryShape::default()
806 };
807 assert!(matches!(
808 shape.validator(),
809 Ok(ValidatorSpec::Format {
810 format: Format::Digits,
811 min_length: Some(6),
812 max_length: Some(6),
813 })
814 ));
815
816 let shape = EntryShape {
818 max_length: Some(8),
819 ..EntryShape::default()
820 };
821 assert!(matches!(
822 shape.validator(),
823 Ok(ValidatorSpec::Format {
824 format: Format::Text,
825 min_length: None,
826 max_length: Some(8),
827 })
828 ));
829
830 assert!(matches!(
831 EntryShape::default().validator(),
832 Ok(ValidatorSpec::NonEmpty)
833 ));
834 assert!(matches!(
835 EntryShape {
836 format: Some(FormatSpec::Pattern {
837 pattern: "[a-z]+".to_string()
838 }),
839 ..EntryShape::default()
840 }
841 .validator(),
842 Ok(ValidatorSpec::Regex(_))
843 ));
844 }
845
846 #[test]
847 fn rejects_an_entry_shape_that_is_two_rules() {
848 assert!(
849 sole(
850 ActionType::EnterValue,
851 &json!({"format": {"pattern": "[0-9]+"}, "length": 6})
852 )
853 .contains("pattern")
854 );
855 assert!(
856 sole(
857 ActionType::EnterSecret,
858 &json!({"length": 6, "min_length": 4})
859 )
860 .contains("'length' is exact")
861 );
862 assert!(
863 sole(
864 ActionType::EnterSecret,
865 &json!({"min_length": 8, "max_length": 6})
866 )
867 .contains("no value fits")
868 );
869 }
870
871 #[test]
872 fn rejects_an_entry_shape_outside_the_vocabulary() {
873 assert!(
874 sole(ActionType::EnterSecret, &json!({"format": "emoji"})).contains("unknown format")
875 );
876 assert!(
877 sole(
878 ActionType::EnterSecret,
879 &json!({"format": {"pattern": "[0-9"}})
880 )
881 .contains("invalid pattern")
882 );
883 assert!(
884 sole(
885 ActionType::EnterSecret,
886 &json!({"format": {"regex": "[0-9]+"}})
887 )
888 .contains("'format' must name a format")
889 );
890 assert!(sole(ActionType::EnterSecret, &json!({"length": 0})).contains("positive integer"));
891 assert!(
892 sole(ActionType::EnterValue, &json!({"format": "paper32"})).contains("enter_secret")
893 );
894 assert!(
895 check(
896 ActionType::EnterSecret,
897 &json!({"format": "paper32", "length": 32})
898 )
899 .is_empty()
900 );
901 assert!(
902 sole(ActionType::EnterSecret, &json!({"length": "six"})).contains("positive integer")
903 );
904 assert!(sole(ActionType::EnterValue, &json!({"format": "bip39"})).contains("enter_secret"));
905 assert!(
906 check(
907 ActionType::EnterSecret,
908 &json!({"format": "bip39", "length": 24})
909 )
910 .is_empty()
911 );
912 assert!(
913 check(
914 ActionType::EnterSecret,
915 &json!({"format": "bip39", "min_length": 12, "max_length": 24})
916 )
917 .is_empty()
918 );
919 assert!(
920 sole(
921 ActionType::EnterSecret,
922 &json!({"format": "bip39", "length": 32})
923 )
924 .contains("12, 15, 18, 21 or 24")
925 );
926 assert!(
927 sole(
928 ActionType::EnterSecret,
929 &json!({"format": "bip39", "min_length": 13, "max_length": 14})
930 )
931 .contains("12, 15, 18, 21 or 24")
932 );
933 assert!(check(ActionType::EnterSecret, &json!({"message": "PIN"})).is_empty());
934 assert!(
935 check(
936 ActionType::EnterSecret,
937 &json!({"message": "Key", "format": "hex", "length": 32})
938 )
939 .is_empty()
940 );
941 }
942}