1mod attestation;
18mod crypto;
19mod entropy;
20mod entry;
21mod piv;
22mod pki;
23mod schema;
24mod sharing;
25mod verification;
26
27pub use attestation::AttestParams;
28pub use crypto::{
29 EncryptDataParams, GenerateKeyParams, ImportKeyParams, KeyPolicyParams, KeyUsage,
30 SignDataParams, UnwrapKeyParams, VerifySignatureParams, WrapKeyParams,
31};
32pub use entropy::GatherEntropyParams;
33pub use entry::EntryParams;
34pub use piv::{
35 AttestSlotParams, PIV_SIGN_ALGORITHMS, PIV_SLOT, PivReadCertificateParams, PivSignParams,
36};
37pub use pki::{GenerateCsrParams, IssueCertificateParams};
38pub(crate) use schema::names;
39pub use schema::{KEYWORDS, generator, with_schema};
40pub use sharing::{EnterShareParams, RevealParams, SplitSecretParams};
41pub use verification::{
42 CheckValueParams, ClockCheckParams, ConfirmParams, MachineInfoParams, OralReadbackParams,
43 ReadbackFormat,
44};
45
46use serde::{Deserialize, Serialize};
47
48use crate::transcript::{Format, ValidatorSpec, compile_pattern};
49use crate::types::ActionType;
50
51#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct ParamError {
57 pub message: String,
59}
60
61#[must_use]
74pub fn cross_field(action: ActionType, with: &serde_json::Value) -> Vec<ParamError> {
75 match action {
76 ActionType::SplitSecret => share_counts(with),
77 ActionType::EnterValue | ActionType::EnterSecret => entry_shape(with),
78
79 ActionType::ClockCheck
80 | ActionType::Confirm
81 | ActionType::CheckValue
82 | ActionType::OralReadback
83 | ActionType::Reveal
84 | ActionType::MachineInfo
85 | ActionType::GenerateKey
86 | ActionType::WrapKey
87 | ActionType::UnwrapKey
88 | ActionType::ImportKey
89 | ActionType::EncryptData
90 | ActionType::DecryptData
91 | ActionType::CombineShares
92 | ActionType::EnterShare
93 | ActionType::ExportPublic
94 | ActionType::SignData
95 | ActionType::VerifySignature
96 | ActionType::Attest
97 | ActionType::GatherEntropy
98 | ActionType::TpmAttest
99 | ActionType::PivReadCertificate
100 | ActionType::PivSign
101 | ActionType::YubikeyAttestSlot
102 | ActionType::IssueCertificate
103 | ActionType::GenerateCsr => Vec::new(),
104 }
105}
106
107fn share_counts(with: &serde_json::Value) -> Vec<ParamError> {
109 let count = |field| with.get(field).and_then(serde_json::Value::as_u64);
110 match (count("threshold"), count("shares")) {
111 (Some(threshold), Some(shares)) if shares < threshold => vec![ParamError {
112 message: "'shares' is below 'threshold', and fewer shares than the threshold \
113 could never reconstruct the secret"
114 .to_string(),
115 }],
116 _ => Vec::new(),
117 }
118}
119
120fn entry_shape(with: &serde_json::Value) -> Vec<ParamError> {
123 let length = |field| {
124 with.get(field)
125 .and_then(serde_json::Value::as_u64)
126 .and_then(|n| usize::try_from(n).ok())
127 };
128 let shape = EntryShape {
129 format: with
130 .get("format")
131 .and_then(|format| serde_json::from_value(format.clone()).ok()),
132 length: length("length"),
133 min_length: length("min_length"),
134 max_length: length("max_length"),
135 };
136 shape
137 .validator()
138 .err()
139 .map(|message| ParamError { message })
140 .into_iter()
141 .collect()
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(untagged)]
150pub enum FormatSpec {
151 Named(Format),
153 Pattern {
155 pattern: String,
157 },
158}
159
160impl schemars::JsonSchema for FormatSpec {
162 fn schema_name() -> std::borrow::Cow<'static, str> {
163 "FormatSpec".into()
164 }
165
166 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
167 let named = generator.subschema_for::<Format>();
168 schemars::json_schema!({
169 "oneOf": [
170 named,
171 {
172 "type": "object",
173 "properties": {
174 "pattern": {
175 "type": "string",
176 "description": "A regular expression the whole value must match.",
177 }
178 },
179 "required": ["pattern"],
180 "additionalProperties": false,
181 }
182 ],
183 "x-message": format!(
184 "'format' must name a format ({}) or be {{ pattern: \"...\" }}",
185 Format::ALL
186 .iter()
187 .map(|format| format.as_str())
188 .collect::<Vec<_>>()
189 .join(", ")
190 ),
191 })
192 }
193}
194
195#[derive(Debug, Clone, Default, PartialEq, Eq)]
201pub struct EntryShape {
202 pub format: Option<FormatSpec>,
204 pub length: Option<usize>,
206 pub min_length: Option<usize>,
208 pub max_length: Option<usize>,
210}
211
212impl EntryShape {
213 pub fn validator(&self) -> Result<ValidatorSpec, String> {
225 let bounded =
226 self.length.is_some() || self.min_length.is_some() || self.max_length.is_some();
227 let format = match &self.format {
228 Some(FormatSpec::Pattern { pattern }) => {
229 if bounded {
230 return Err("a pattern says the whole shape of the value, so 'length', \
231 'min_length' and 'max_length' cannot be given beside it"
232 .to_string());
233 }
234 compile_pattern(pattern)?;
235 return Ok(ValidatorSpec::Regex(pattern.clone()));
236 }
237 Some(FormatSpec::Named(format)) => Some(*format),
238 None => None,
239 };
240 let (min_length, max_length) = match (self.length, self.min_length, self.max_length) {
241 (Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
242 return Err(
243 "'length' is exact, so 'min_length' and 'max_length' cannot be given \
244 beside it"
245 .to_string(),
246 );
247 }
248 (Some(length), None, None) => (Some(length), Some(length)),
249 (None, min, max) => (min, max),
250 };
251 if let (Some(min), Some(max)) = (min_length, max_length)
252 && min > max
253 {
254 return Err("'min_length' is above 'max_length', so no value fits".to_string());
255 }
256 if format == Some(Format::Bip39)
258 && !crate::bip39::phrase_lengths().any(|n| {
259 min_length.is_none_or(|min| n >= min) && max_length.is_none_or(|max| n <= max)
260 })
261 {
262 return Err(format!(
263 "a BIP-39 phrase has {} words, and the length given holds none of them",
264 crate::bip39::PHRASE_LENGTHS
265 ));
266 }
267 match format {
268 None if !bounded => Ok(ValidatorSpec::NonEmpty),
269 format => Ok(ValidatorSpec::Format {
270 format: format.unwrap_or(Format::Text),
271 min_length,
272 max_length,
273 }),
274 }
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use serde_json::json;
282
283 fn sole(action: ActionType, with: &serde_json::Value) -> String {
284 let errors = cross_field(action, with);
285 let [error] = errors.as_slice() else {
286 panic!("expected exactly one error, got {errors:?}");
287 };
288 error.message.clone()
289 }
290
291 #[test]
292 fn a_split_needs_at_least_as_many_shares_as_its_threshold() {
293 assert!(
294 sole(
295 ActionType::SplitSecret,
296 &json!({"threshold": 4, "shares": 3})
297 )
298 .contains("fewer shares than the threshold")
299 );
300 assert!(
301 cross_field(
302 ActionType::SplitSecret,
303 &json!({"threshold": 3, "shares": 3})
304 )
305 .is_empty()
306 );
307 assert!(cross_field(ActionType::SplitSecret, &json!({"shares": 3})).is_empty());
309 }
310
311 #[test]
313 fn an_entry_shape_becomes_one_rule() {
314 let shape = EntryShape {
315 format: Some(FormatSpec::Named(Format::Digits)),
316 length: Some(6),
317 ..EntryShape::default()
318 };
319 assert!(matches!(
320 shape.validator(),
321 Ok(ValidatorSpec::Format {
322 format: Format::Digits,
323 min_length: Some(6),
324 max_length: Some(6),
325 })
326 ));
327
328 let shape = EntryShape {
330 max_length: Some(8),
331 ..EntryShape::default()
332 };
333 assert!(matches!(
334 shape.validator(),
335 Ok(ValidatorSpec::Format {
336 format: Format::Text,
337 min_length: None,
338 max_length: Some(8),
339 })
340 ));
341
342 assert!(matches!(
343 EntryShape::default().validator(),
344 Ok(ValidatorSpec::NonEmpty)
345 ));
346 assert!(matches!(
347 EntryShape {
348 format: Some(FormatSpec::Pattern {
349 pattern: "[a-z]+".to_string()
350 }),
351 ..EntryShape::default()
352 }
353 .validator(),
354 Ok(ValidatorSpec::Regex(_))
355 ));
356 }
357
358 #[test]
359 fn rejects_an_entry_shape_no_value_fits() {
360 assert!(
361 sole(
362 ActionType::EnterSecret,
363 &json!({"min_length": 8, "max_length": 6})
364 )
365 .contains("no value fits")
366 );
367 assert!(
368 sole(
369 ActionType::EnterSecret,
370 &json!({"format": {"pattern": "[0-9"}})
371 )
372 .contains("invalid pattern")
373 );
374 assert!(
375 sole(
376 ActionType::EnterSecret,
377 &json!({"format": "bip39", "length": 32})
378 )
379 .contains("12, 15, 18, 21 or 24")
380 );
381 assert!(
382 sole(
383 ActionType::EnterSecret,
384 &json!({"format": "bip39", "min_length": 13, "max_length": 14})
385 )
386 .contains("12, 15, 18, 21 or 24")
387 );
388 for with in [
389 json!({"format": "bip39", "length": 24}),
390 json!({"format": "bip39", "min_length": 12, "max_length": 24}),
391 json!({"message": "Key", "format": "hex", "length": 32}),
392 json!({"message": "PIN"}),
393 ] {
394 assert!(
395 cross_field(ActionType::EnterSecret, &with).is_empty(),
396 "{with}"
397 );
398 }
399 }
400}
401
402#[cfg(test)]
403mod schema_tests {
404 use super::*;
405 use crate::ActionType;
406 use serde_json::Value;
407
408 #[test]
411 fn every_schema_keeps_to_the_keywords() {
412 fn walk(value: &Value, path: &str, found: &mut Vec<String>) {
413 let Value::Object(object) = value else {
414 return;
415 };
416 if object.contains_key("$ref") && object.contains_key("x-message") {
420 found.push(format!("{path}: x-message beside $ref"));
421 }
422 for (key, child) in object {
423 if !KEYWORDS.contains(&key.as_str()) {
424 found.push(format!("{path}/{key}"));
425 }
426 let here = format!("{path}/{key}");
427 match (key.as_str(), child) {
428 ("properties" | "$defs", Value::Object(map)) => {
430 for (name, schema) in map {
431 walk(schema, &format!("{here}/{name}"), found);
432 }
433 }
434 ("allOf" | "oneOf", Value::Array(items)) => {
436 for (index, item) in items.iter().enumerate() {
437 walk(item, &format!("{here}/{index}"), found);
438 }
439 }
440 ("items" | "if" | "then" | "not" | "additionalProperties", _) => {
442 walk(child, &here, found);
443 }
444 _ => {}
446 }
447 }
448 }
449 for &action in ActionType::ALL {
450 let mut found = Vec::new();
451 walk(&with_schema(action).to_value(), "", &mut found);
452 assert!(found.is_empty(), "{action} uses {found:?}");
453 }
454 }
455}