Skip to main content

rite_model/params/
schema.rs

1//! The JSON Schema of each action's `with:` block, generated from the params
2//! types.
3//!
4//! The schema is the check: it says which keys an action takes, which it
5//! requires, and what each value may be. It is identical in every build. What
6//! one binary cannot carry out is a separate question, answered by the action
7//! handler.
8//!
9//! Generated schemas use a small set of keywords, so that a validator for that
10//! set alone is enough to check them. [`KEYWORDS`] is that set, and a test
11//! holds every generated schema to it.
12
13use schemars::generate::SchemaSettings;
14use schemars::transform::{Transform, transform_subschemas};
15use schemars::{Schema, SchemaGenerator, json_schema};
16use serde_json::Value;
17
18use super::{
19    AttestParams, AttestSlotParams, CheckValueParams, ClockCheckParams, ConfirmParams,
20    EncryptDataParams, EnterShareParams, EntryParams, GatherEntropyParams, GenerateCsrParams,
21    GenerateKeyParams, ImportKeyParams, IssueCertificateParams, MachineInfoParams,
22    OralReadbackParams, PivReadCertificateParams, PivSignParams, RevealParams, SignDataParams,
23    SplitSecretParams, UnwrapKeyParams, VerifySignatureParams, WrapKeyParams,
24};
25use crate::{ActionType, Format};
26
27/// The keywords a generated schema may use: the ones that constrain a value,
28/// the ones that combine schemas, and the annotations.
29///
30/// `x-message` is Rite's own annotation: the message to report when the
31/// schema object carrying it, or one it combines, rejects a value.
32pub const KEYWORDS: &[&str] = &[
33    "type",
34    "enum",
35    "const",
36    "pattern",
37    "minimum",
38    "maximum",
39    "minItems",
40    "items",
41    "properties",
42    "required",
43    "additionalProperties",
44    "allOf",
45    "oneOf",
46    "if",
47    "then",
48    "not",
49    "$ref",
50    "$defs",
51    "description",
52    "default",
53    "title",
54    "x-message",
55];
56
57/// The largest integer a JSON number holds exactly, `2^53 - 1`.
58const MAX_SAFE_INTEGER: i64 = (1 << 53) - 1;
59
60/// A generator with the settings every params schema is made with.
61#[must_use]
62pub fn generator() -> SchemaGenerator {
63    SchemaSettings::draft2020_12()
64        .with_transform(Strict)
65        .into_generator()
66}
67
68/// The whole schema of `action`'s `with:` block, with the definitions it
69/// refers to.
70#[must_use]
71pub fn with_schema(action: ActionType) -> Schema {
72    let mut generator = generator();
73    let mut schema = action.params_schema(&mut generator);
74    Strict.transform(&mut schema);
75    let definitions = generator.take_definitions(true);
76    if !definitions.is_empty() {
77        schema.insert("$defs".to_string(), Value::Object(definitions));
78    }
79    schema
80}
81
82impl ActionType {
83    /// The schema of this action's `with:` block.
84    ///
85    /// The one per-action list `with:` has: a new action fails to compile
86    /// here until it names its params type. An action that takes no
87    /// parameters accepts an empty block and nothing else.
88    pub fn params_schema(self, generator: &mut SchemaGenerator) -> Schema {
89        match self {
90            ActionType::ClockCheck => generator.subschema_for::<ClockCheckParams>(),
91            ActionType::Confirm => generator.subschema_for::<ConfirmParams>(),
92            ActionType::CheckValue => generator.subschema_for::<CheckValueParams>(),
93            ActionType::OralReadback => generator.subschema_for::<OralReadbackParams>(),
94            ActionType::Reveal => generator.subschema_for::<RevealParams>(),
95            ActionType::MachineInfo => generator.subschema_for::<MachineInfoParams>(),
96            ActionType::EnterValue => enter_value(generator),
97            ActionType::EnterSecret => generator.subschema_for::<EntryParams>(),
98            ActionType::GenerateKey => generator.subschema_for::<GenerateKeyParams>(),
99            ActionType::WrapKey => generator.subschema_for::<WrapKeyParams>(),
100            ActionType::UnwrapKey => generator.subschema_for::<UnwrapKeyParams>(),
101            ActionType::ImportKey => generator.subschema_for::<ImportKeyParams>(),
102            ActionType::EncryptData => generator.subschema_for::<EncryptDataParams>(),
103            ActionType::SplitSecret => generator.subschema_for::<SplitSecretParams>(),
104            ActionType::EnterShare => generator.subschema_for::<EnterShareParams>(),
105            ActionType::SignData => generator.subschema_for::<SignDataParams>(),
106            ActionType::VerifySignature => generator.subschema_for::<VerifySignatureParams>(),
107            ActionType::Attest => generator.subschema_for::<AttestParams>(),
108            ActionType::GatherEntropy => generator.subschema_for::<GatherEntropyParams>(),
109            ActionType::PivReadCertificate => generator.subschema_for::<PivReadCertificateParams>(),
110            ActionType::PivSign => generator.subschema_for::<PivSignParams>(),
111            ActionType::YubikeyAttestSlot => generator.subschema_for::<AttestSlotParams>(),
112            ActionType::IssueCertificate => generator.subschema_for::<IssueCertificateParams>(),
113            ActionType::GenerateCsr => generator.subschema_for::<GenerateCsrParams>(),
114            // These take their inputs through `reads:` alone, and
115            // `tpm_attest` has no handler in any build yet.
116            ActionType::ExportPublic
117            | ActionType::DecryptData
118            | ActionType::CombineShares
119            | ActionType::TpmAttest => json_schema!({
120                "type": "object",
121                "properties": {},
122                "additionalProperties": false,
123            }),
124        }
125    }
126}
127
128/// `enter_value` is `enter_secret` without the formats written down on a
129/// sheet or read off a device: such a value is a secret, and the transcript
130/// carries what this step was answered.
131fn enter_value(generator: &mut SchemaGenerator) -> Schema {
132    let entry = generator.subschema_for::<EntryParams>();
133    let secret: Vec<&str> = Format::ALL
134        .iter()
135        .filter(|format| format.rows().is_some())
136        .map(|format| format.as_str())
137        .collect();
138    json_schema!({
139        "allOf": [
140            entry,
141            {
142                "properties": {
143                    "format": {
144                        "not": { "enum": secret },
145                        "x-message": format!(
146                            "{} are for a value written down, which is a secret: use enter_secret",
147                            secret.join(" and ")
148                        ),
149                    }
150                }
151            }
152        ]
153    })
154}
155
156/// A JSON Schema `enum` of names, for a vocabulary narrowed by an action.
157pub(crate) fn names<'a>(names: impl Iterator<Item = &'a str>) -> Schema {
158    let names: Vec<&str> = names.collect();
159    json_schema!({ "type": "string", "enum": names })
160}
161
162/// Add a rule over several fields to a struct's schema, beside the ones its
163/// fields carry.
164pub(super) fn add_rule(schema: &mut Schema, rule: Value) {
165    let rules = schema
166        .ensure_object()
167        .entry("allOf")
168        .or_insert_with(|| Value::Array(Vec::new()));
169    if let Value::Array(rules) = rules {
170        rules.push(rule);
171    }
172}
173
174/// The schema of one field of a struct, to add to what its type says.
175///
176/// # Panics
177///
178/// If the struct has no such field, which is a mistake in this module.
179pub(super) fn property<'a>(
180    schema: &'a mut Schema,
181    name: &str,
182) -> &'a mut serde_json::Map<String, Value> {
183    schema
184        .get_mut("properties")
185        .and_then(|properties| properties.get_mut(name))
186        .and_then(Value::as_object_mut)
187        .unwrap_or_else(|| panic!("the schema has no property '{name}'"))
188}
189
190/// What every generated schema is reduced to, so it stays within
191/// [`KEYWORDS`] and says what serde accepts.
192///
193/// - An integer's Rust type becomes the bound it implies, in place of
194///   `format: uint8` and the like, which name a Rust type.
195/// - An optional field takes its value's schema. serde reads `null` as
196///   absent, but nothing in a ceremony means it, so the schema leaves it out
197///   rather than adding `null` to every optional field.
198/// - A `null` default says nothing and is dropped.
199#[derive(Clone)]
200struct Strict;
201
202impl Transform for Strict {
203    fn transform(&mut self, schema: &mut Schema) {
204        if let Some(Value::Array(kinds)) = schema.get_mut("type") {
205            kinds.retain(|kind| kind != "null");
206            if let [only] = kinds.as_slice() {
207                let only = only.clone();
208                schema.insert("type".to_string(), only);
209            }
210        }
211        if let Some(Value::Array(branches)) = schema.get("anyOf")
212            && let [value, null] = branches.as_slice()
213            && *null == serde_json::json!({ "type": "null" })
214            && let Value::Object(value) = value.clone()
215        {
216            schema.remove("anyOf");
217            for (key, keyword) in value {
218                schema.insert(key, keyword);
219            }
220        }
221        if schema.get("default") == Some(&Value::Null) {
222            schema.remove("default");
223        }
224        if schema.get("type").and_then(Value::as_str) == Some("integer") {
225            let (low, high) = match schema.remove("format").as_ref().and_then(Value::as_str) {
226                Some("uint8") => (0, i64::from(u8::MAX)),
227                Some("uint16") => (0, i64::from(u16::MAX)),
228                Some("uint32") => (0, i64::from(u32::MAX)),
229                Some("uint" | "uint64") => (0, MAX_SAFE_INTEGER),
230                Some("int8") => (i64::from(i8::MIN), i64::from(i8::MAX)),
231                Some("int16") => (i64::from(i16::MIN), i64::from(i16::MAX)),
232                Some("int32") => (i64::from(i32::MIN), i64::from(i32::MAX)),
233                _ => (-MAX_SAFE_INTEGER, MAX_SAFE_INTEGER),
234            };
235            let minimum = schema
236                .get("minimum")
237                .and_then(Value::as_i64)
238                .map_or(low, |min| min.max(low));
239            let maximum = schema
240                .get("maximum")
241                .and_then(Value::as_i64)
242                .map_or(high, |max| max.min(high));
243            schema.insert("minimum".to_string(), minimum.into());
244            schema.insert("maximum".to_string(), maximum.into());
245        }
246        transform_subschemas(self, schema);
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use schemars::JsonSchema;
254    use serde_json::json;
255
256    /// An integer field's schema is bounded by its Rust type, signed or not,
257    /// and a bound the field sets itself is kept when it is tighter.
258    #[test]
259    fn an_integer_is_bounded_by_its_rust_type() {
260        #[derive(JsonSchema)]
261        #[allow(dead_code)]
262        struct Integers {
263            small: i8,
264            medium: i16,
265            large: i32,
266            count: u8,
267            #[schemars(range(min = 1))]
268            shares: u8,
269        }
270        let schema = generator().root_schema_for::<Integers>();
271        let value = schema.as_value();
272        let bounds = |name: &str| {
273            let at = |keyword: &str| {
274                value
275                    .pointer(&format!("/properties/{name}/{keyword}"))
276                    .cloned()
277            };
278            (at("minimum"), at("maximum"))
279        };
280        let expect = |low: i64, high: i64| (Some(json!(low)), Some(json!(high)));
281        assert_eq!(bounds("small"), expect(-128, 127));
282        assert_eq!(bounds("medium"), expect(-32768, 32767));
283        assert_eq!(bounds("large"), expect(i32::MIN.into(), i32::MAX.into()));
284        assert_eq!(bounds("count"), expect(0, 255));
285        assert_eq!(bounds("shares"), expect(1, 255));
286        assert!(value.pointer("/properties/small/format").is_none());
287    }
288}