1use crate::types::{ActionType, CertProfile};
16use rite_sdk::{KeyAlgorithm, KeyUsages, SignAlgorithm, WrapScheme};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ParamError {
24 pub message: String,
26}
27
28#[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
90fn 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
114fn 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
145fn 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
165fn 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 #[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 #[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 #[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 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 #[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 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}