Skip to main content

rite_model/
params.rs

1//! Rules over a step's `with:` values that hold in every build.
2//!
3//! These sit beside [`ActionType::required_with_fields`] and
4//! [`ActionType::reads_contract`]: what an action requires of its block, in
5//! one place both the resolver and the runtime can read. Keeping them here lets
6//! an editor report them, since the language server links the model and the
7//! resolver but no backend.
8//!
9//! Only rules that travel with the document belong here. `RSA-9999` is not a
10//! key algorithm on any machine, so rejecting it is portable. Whether *this*
11//! binary can generate an ML-DSA key is a property of the build doing the
12//! checking, and that question is answered by the action handler instead, at
13//! the point where a backend exists to ask.
14
15use crate::types::{ActionType, CertProfile};
16use rite_sdk::{KeyAlgorithm, KeyUsages, SignAlgorithm, WrapScheme};
17
18/// A `with:` value an action cannot accept.
19///
20/// The message names the field it is about, so a reader with no span still
21/// knows where to look.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ParamError {
24    /// What is wrong, phrased for the ceremony author.
25    pub message: String,
26}
27
28/// Check the literal part of a step's `with:` block.
29///
30/// `with` is the projection produced by
31/// [`literal_expr_value`](crate::expression::literal_expr_value), so a field
32/// whose value is an expression is *absent* rather than wrong. Nothing here
33/// may report a field as missing: required fields are
34/// [`ActionType::required_with_fields`]'s job, and a required field is
35/// routinely written as `${param.x}`.
36#[must_use]
37pub fn check(action: ActionType, with: &serde_json::Value) -> Vec<ParamError> {
38    match action {
39        ActionType::GenerateKey => {
40            let mut errors = named_value(with, "algorithm", |name| {
41                name.parse::<KeyAlgorithm>()
42                    .map_err(|_| format!("unknown key algorithm '{name}'"))
43            });
44            errors.extend(key_usages(with));
45            errors
46        }
47        ActionType::SignData | ActionType::VerifySignature => {
48            named_value(with, "algorithm", |name| {
49                name.parse::<SignAlgorithm>()
50                    .map_err(|_| format!("unknown signature algorithm '{name}'"))
51            })
52        }
53        ActionType::IssueCertificate => named_value(with, "profile", |name| {
54            name.parse::<CertProfile>()
55                .map_err(|_| format!("unknown certificate profile '{name}'"))
56        }),
57        ActionType::WrapKey => {
58            let mut errors = fingerprint(with, "expect_recipient");
59            errors.extend(named_value(with, "scheme", |name| {
60                name.parse::<WrapScheme>()
61                    .map_err(|_| format!("unknown wrapping scheme '{name}'"))
62            }));
63            errors
64        }
65        ActionType::UnwrapKey => {
66            let mut errors = key_identity(with, "expect_key");
67            errors.extend(named_value(with, "algorithm", |name| {
68                name.parse::<KeyAlgorithm>()
69                    .map_err(|_| format!("unknown key algorithm '{name}'"))
70            }));
71            errors
72        }
73
74        ActionType::ClockCheck
75        | ActionType::Confirm
76        | ActionType::CheckValue
77        | ActionType::OralReadback
78        | ActionType::MachineInfo
79        | ActionType::ExportPublic
80        | ActionType::Attest
81        | ActionType::GatherEntropy
82        | ActionType::TpmAttest
83        | ActionType::PivReadCertificate
84        | ActionType::PivSign
85        | ActionType::YubikeyAttestSlot
86        | ActionType::GenerateCsr => Vec::new(),
87    }
88}
89
90/// Apply `parse` to a present string field.
91///
92/// An absent field is deferred, not missing. A field holding something other
93/// than a string is wrong whatever the value would have been.
94fn named_value<T>(
95    with: &serde_json::Value,
96    field: &'static str,
97    parse: impl FnOnce(&str) -> Result<T, String>,
98) -> Vec<ParamError> {
99    let Some(value) = with.get(field) else {
100        return Vec::new();
101    };
102    let Some(name) = value.as_str() else {
103        return vec![ParamError {
104            message: format!("'{field}' must be a string, found {value}"),
105        }];
106    };
107    parse(name)
108        .err()
109        .map(|message| ParamError { message })
110        .into_iter()
111        .collect()
112}
113
114/// Check the names under `policy: { usages: [...] }`.
115///
116/// These are PKCS#11 usages, what the token permits the key to do. The X.509
117/// `KeyUsage` extension is a different thing and comes from `profile:` on
118/// `issue_certificate`.
119fn key_usages(with: &serde_json::Value) -> Vec<ParamError> {
120    const FIELD: &str = "policy.usages";
121    let Some(usages) = with.get("policy").and_then(|p| p.get("usages")) else {
122        return Vec::new();
123    };
124    let Some(names) = usages.as_array() else {
125        return vec![ParamError {
126            message: format!("'{FIELD}' must be a list, found {usages}"),
127        }];
128    };
129    names
130        .iter()
131        .filter_map(|name| {
132            let known = name
133                .as_str()
134                .is_some_and(|name| KeyUsages::usage_named(name).is_some());
135            (!known).then(|| ParamError {
136                message: format!(
137                    "unknown key usage {name} under '{FIELD}'. Supported: {}",
138                    KeyUsages::all_names().join(", ")
139                ),
140            })
141        })
142        .collect()
143}
144
145/// A fingerprint the ceremony declares in advance, `sha256:<64 hex digits>`.
146///
147/// A value in any other shape can never equal what the runtime computes, so
148/// the step would fail in the room, after the keys exist. The runtime renders
149/// every fingerprint in lowercase, so uppercase hex is one such shape and is
150/// rejected here rather than at the step.
151fn fingerprint(with: &serde_json::Value, field: &'static str) -> Vec<ParamError> {
152    named_value(with, field, |value| {
153        let hex = value.strip_prefix("sha256:").unwrap_or_default();
154        if hex.len() == 64 && hex.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
155            Ok(())
156        } else {
157            Err(format!(
158                "'{field}' must be a fingerprint of the form \
159                 'sha256:<64 lowercase hex digits>', found '{value}'"
160            ))
161        }
162    })
163}
164
165/// A key's identity the ceremony declares in advance, in whichever form the
166/// key has one: `sha256:<64 hex digits>` over the public half of a keypair, or
167/// `cmac-aes:<6 hex digits>` for a symmetric key, which has no public half.
168///
169/// Same argument as [`fingerprint`]: a value in neither shape can never equal
170/// what the runtime computes, so the mismatch is worth reporting before the
171/// room rather than during it.
172fn key_identity(with: &serde_json::Value, field: &'static str) -> Vec<ParamError> {
173    named_value(with, field, |value| {
174        let lower_hex = |s: &str, len: usize| {
175            s.len() == len && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
176        };
177        let matches_form = match value.split_once(':') {
178            Some(("sha256", hex)) => lower_hex(hex, 64),
179            Some(("cmac-aes", hex)) => lower_hex(hex, 6),
180            _ => false,
181        };
182        if matches_form {
183            Ok(())
184        } else {
185            Err(format!(
186                "'{field}' must name the key either as 'sha256:<64 lowercase hex digits>', \
187                 the fingerprint of a public key, or as 'cmac-aes:<6 lowercase hex digits>', \
188                 the check value of a symmetric key. Found '{value}'"
189            ))
190        }
191    })
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use serde_json::json;
198
199    fn sole(action: ActionType, with: &serde_json::Value) -> String {
200        let errors = check(action, with);
201        let [error] = errors.as_slice() else {
202            panic!("expected exactly one error, got {errors:?}");
203        };
204        error.message.clone()
205    }
206
207    #[test]
208    fn rejects_a_value_outside_the_vocabulary() {
209        assert!(
210            sole(ActionType::GenerateKey, &json!({"algorithm": "RSA-9999"}))
211                .contains("unknown key algorithm")
212        );
213        assert!(
214            sole(ActionType::SignData, &json!({"algorithm": "RSA-PSS-SHA1"}))
215                .contains("unknown signature algorithm")
216        );
217        assert!(
218            sole(
219                ActionType::IssueCertificate,
220                &json!({"profile": "wildcard"})
221            )
222            .contains("unknown certificate profile")
223        );
224        assert!(
225            sole(
226                ActionType::WrapKey,
227                &json!({"expect_recipient": "deadbeef"})
228            )
229            .contains("sha256:<64 lowercase hex digits>")
230        );
231    }
232
233    /// The runtime renders every fingerprint in lowercase, so an uppercase one
234    /// would pass resolution and then never match, failing in the room against
235    /// the very key it names.
236    #[test]
237    fn rejects_a_fingerprint_that_is_not_lowercase() {
238        let upper = format!("sha256:{}", "AB".repeat(32));
239        let message = sole(ActionType::WrapKey, &json!({ "expect_recipient": upper }));
240        assert!(message.contains("lowercase"), "{message}");
241
242        let lower = format!("sha256:{}", "ab".repeat(32));
243        assert!(
244            check(ActionType::WrapKey, &json!({ "expect_recipient": lower })).is_empty(),
245            "the shape the runtime produces is the shape that passes"
246        );
247    }
248
249    /// A scheme name outside the vocabulary would fail in the room, after the
250    /// keys exist, so resolution rejects it.
251    #[test]
252    fn rejects_a_wrapping_scheme_outside_the_vocabulary() {
253        let message = sole(ActionType::WrapKey, &json!({"scheme": "CMS-RSA-CBC"}));
254        assert!(message.contains("unknown wrapping scheme"), "{message}");
255
256        assert!(
257            check(
258                ActionType::WrapKey,
259                &json!({"scheme": "RSA-AES-KEY-WRAP-SHA256"})
260            )
261            .is_empty()
262        );
263        assert!(
264            check(ActionType::WrapKey, &json!({})).is_empty(),
265            "deferred"
266        );
267    }
268
269    #[test]
270    fn accepts_the_vocabulary() {
271        for (action, with) in [
272            (ActionType::GenerateKey, json!({"algorithm": "RSA-4096"})),
273            (ActionType::SignData, json!({"algorithm": "RSA-PSS-SHA256"})),
274            (ActionType::IssueCertificate, json!({"profile": "root_ca"})),
275            (
276                ActionType::UnwrapKey,
277                json!({"expect_key": format!("sha256:{}", "ab".repeat(32))}),
278            ),
279            (
280                ActionType::UnwrapKey,
281                json!({"algorithm": "AES-256", "expect_key": "cmac-aes:763cbc"}),
282            ),
283        ] {
284            assert!(check(action, &with).is_empty(), "{action} rejected {with}");
285        }
286    }
287
288    /// `expect_key` names the recovered key in whichever form it has one, so
289    /// both are accepted and anything else is refused before the room.
290    #[test]
291    fn expect_key_takes_a_fingerprint_or_a_check_value() {
292        let message = sole(
293            ActionType::UnwrapKey,
294            &json!({"expect_key": "cmac-aes:763CBC"}),
295        );
296        assert!(message.contains("lowercase"), "{message}");
297
298        // A check value is three bytes, so a fingerprint's length under the
299        // check value's prefix is not a check value.
300        let message = sole(
301            ActionType::UnwrapKey,
302            &json!({"expect_key": format!("cmac-aes:{}", "ab".repeat(32))}),
303        );
304        assert!(message.contains("6 lowercase hex digits"), "{message}");
305
306        let message = sole(ActionType::UnwrapKey, &json!({"expect_key": "763cbc"}));
307        assert!(
308            message.contains("sha256:") && message.contains("cmac-aes:"),
309            "{message}"
310        );
311    }
312
313    /// Nothing travels with a wrapped key saying what it is, so `algorithm:`
314    /// declares it, and a name outside the vocabulary fails here rather than
315    /// after the blob has been decrypted.
316    #[test]
317    fn rejects_an_unwrap_algorithm_outside_the_vocabulary() {
318        let message = sole(ActionType::UnwrapKey, &json!({"algorithm": "AES-192"}));
319        assert!(message.contains("unknown key algorithm"), "{message}");
320    }
321
322    #[test]
323    fn an_absent_field_is_deferred_not_missing() {
324        // The projection drops `algorithm: ${param.algo}`, and a required
325        // field written as an expression must not be reported here.
326        assert!(check(ActionType::GenerateKey, &json!({})).is_empty());
327        assert!(check(ActionType::IssueCertificate, &json!({})).is_empty());
328    }
329
330    #[test]
331    fn rejects_an_unknown_key_usage() {
332        let message = sole(
333            ActionType::GenerateKey,
334            &json!({"policy": {"usages": ["sign", "key_cert_sign"]}}),
335        );
336        assert!(message.contains("unknown key usage"), "{message}");
337        assert!(
338            message.contains("wrap"),
339            "the message should list what is available: {message}"
340        );
341    }
342
343    #[test]
344    fn accepts_the_pkcs11_usages() {
345        let with = json!({"policy": {"usages": ["sign", "verify", "wrap", "unwrap", "derive"]}});
346        assert!(check(ActionType::GenerateKey, &with).is_empty());
347    }
348
349    #[test]
350    fn a_non_string_value_is_rejected_whatever_it_would_have_meant() {
351        assert!(
352            sole(ActionType::GenerateKey, &json!({"algorithm": 4096})).contains("must be a string")
353        );
354    }
355}