use crate::types::{ActionType, CertProfile};
use rite_sdk::{KeyAlgorithm, KeyUsages, SignAlgorithm, WrapScheme};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParamError {
pub message: String,
}
#[must_use]
pub fn check(action: ActionType, with: &serde_json::Value) -> Vec<ParamError> {
match action {
ActionType::GenerateKey => {
let mut errors = named_value(with, "algorithm", |name| {
name.parse::<KeyAlgorithm>()
.map_err(|_| format!("unknown key algorithm '{name}'"))
});
errors.extend(key_usages(with));
errors
}
ActionType::SignData | ActionType::VerifySignature => {
named_value(with, "algorithm", |name| {
name.parse::<SignAlgorithm>()
.map_err(|_| format!("unknown signature algorithm '{name}'"))
})
}
ActionType::IssueCertificate => named_value(with, "profile", |name| {
name.parse::<CertProfile>()
.map_err(|_| format!("unknown certificate profile '{name}'"))
}),
ActionType::WrapKey => {
let mut errors = fingerprint(with, "expect_recipient");
errors.extend(named_value(with, "scheme", |name| {
name.parse::<WrapScheme>()
.map_err(|_| format!("unknown wrapping scheme '{name}'"))
}));
errors
}
ActionType::UnwrapKey => {
let mut errors = key_identity(with, "expect_key");
errors.extend(named_value(with, "algorithm", |name| {
name.parse::<KeyAlgorithm>()
.map_err(|_| format!("unknown key algorithm '{name}'"))
}));
errors
}
ActionType::ClockCheck
| ActionType::Confirm
| ActionType::CheckValue
| ActionType::OralReadback
| ActionType::MachineInfo
| ActionType::ExportPublic
| ActionType::Attest
| ActionType::GatherEntropy
| ActionType::TpmAttest
| ActionType::PivReadCertificate
| ActionType::PivSign
| ActionType::YubikeyAttestSlot
| ActionType::GenerateCsr => Vec::new(),
}
}
fn named_value<T>(
with: &serde_json::Value,
field: &'static str,
parse: impl FnOnce(&str) -> Result<T, String>,
) -> Vec<ParamError> {
let Some(value) = with.get(field) else {
return Vec::new();
};
let Some(name) = value.as_str() else {
return vec![ParamError {
message: format!("'{field}' must be a string, found {value}"),
}];
};
parse(name)
.err()
.map(|message| ParamError { message })
.into_iter()
.collect()
}
fn key_usages(with: &serde_json::Value) -> Vec<ParamError> {
const FIELD: &str = "policy.usages";
let Some(usages) = with.get("policy").and_then(|p| p.get("usages")) else {
return Vec::new();
};
let Some(names) = usages.as_array() else {
return vec![ParamError {
message: format!("'{FIELD}' must be a list, found {usages}"),
}];
};
names
.iter()
.filter_map(|name| {
let known = name
.as_str()
.is_some_and(|name| KeyUsages::usage_named(name).is_some());
(!known).then(|| ParamError {
message: format!(
"unknown key usage {name} under '{FIELD}'. Supported: {}",
KeyUsages::all_names().join(", ")
),
})
})
.collect()
}
fn fingerprint(with: &serde_json::Value, field: &'static str) -> Vec<ParamError> {
named_value(with, field, |value| {
let hex = value.strip_prefix("sha256:").unwrap_or_default();
if hex.len() == 64 && hex.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
Ok(())
} else {
Err(format!(
"'{field}' must be a fingerprint of the form \
'sha256:<64 lowercase hex digits>', found '{value}'"
))
}
})
}
fn key_identity(with: &serde_json::Value, field: &'static str) -> Vec<ParamError> {
named_value(with, field, |value| {
let lower_hex = |s: &str, len: usize| {
s.len() == len && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
};
let matches_form = match value.split_once(':') {
Some(("sha256", hex)) => lower_hex(hex, 64),
Some(("cmac-aes", hex)) => lower_hex(hex, 6),
_ => false,
};
if matches_form {
Ok(())
} else {
Err(format!(
"'{field}' must name the key either as 'sha256:<64 lowercase hex digits>', \
the fingerprint of a public key, or as 'cmac-aes:<6 lowercase hex digits>', \
the check value of a symmetric key. Found '{value}'"
))
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn sole(action: ActionType, with: &serde_json::Value) -> String {
let errors = check(action, with);
let [error] = errors.as_slice() else {
panic!("expected exactly one error, got {errors:?}");
};
error.message.clone()
}
#[test]
fn rejects_a_value_outside_the_vocabulary() {
assert!(
sole(ActionType::GenerateKey, &json!({"algorithm": "RSA-9999"}))
.contains("unknown key algorithm")
);
assert!(
sole(ActionType::SignData, &json!({"algorithm": "RSA-PSS-SHA1"}))
.contains("unknown signature algorithm")
);
assert!(
sole(
ActionType::IssueCertificate,
&json!({"profile": "wildcard"})
)
.contains("unknown certificate profile")
);
assert!(
sole(
ActionType::WrapKey,
&json!({"expect_recipient": "deadbeef"})
)
.contains("sha256:<64 lowercase hex digits>")
);
}
#[test]
fn rejects_a_fingerprint_that_is_not_lowercase() {
let upper = format!("sha256:{}", "AB".repeat(32));
let message = sole(ActionType::WrapKey, &json!({ "expect_recipient": upper }));
assert!(message.contains("lowercase"), "{message}");
let lower = format!("sha256:{}", "ab".repeat(32));
assert!(
check(ActionType::WrapKey, &json!({ "expect_recipient": lower })).is_empty(),
"the shape the runtime produces is the shape that passes"
);
}
#[test]
fn rejects_a_wrapping_scheme_outside_the_vocabulary() {
let message = sole(ActionType::WrapKey, &json!({"scheme": "CMS-RSA-CBC"}));
assert!(message.contains("unknown wrapping scheme"), "{message}");
assert!(
check(
ActionType::WrapKey,
&json!({"scheme": "RSA-AES-KEY-WRAP-SHA256"})
)
.is_empty()
);
assert!(
check(ActionType::WrapKey, &json!({})).is_empty(),
"deferred"
);
}
#[test]
fn accepts_the_vocabulary() {
for (action, with) in [
(ActionType::GenerateKey, json!({"algorithm": "RSA-4096"})),
(ActionType::SignData, json!({"algorithm": "RSA-PSS-SHA256"})),
(ActionType::IssueCertificate, json!({"profile": "root_ca"})),
(
ActionType::UnwrapKey,
json!({"expect_key": format!("sha256:{}", "ab".repeat(32))}),
),
(
ActionType::UnwrapKey,
json!({"algorithm": "AES-256", "expect_key": "cmac-aes:763cbc"}),
),
] {
assert!(check(action, &with).is_empty(), "{action} rejected {with}");
}
}
#[test]
fn expect_key_takes_a_fingerprint_or_a_check_value() {
let message = sole(
ActionType::UnwrapKey,
&json!({"expect_key": "cmac-aes:763CBC"}),
);
assert!(message.contains("lowercase"), "{message}");
let message = sole(
ActionType::UnwrapKey,
&json!({"expect_key": format!("cmac-aes:{}", "ab".repeat(32))}),
);
assert!(message.contains("6 lowercase hex digits"), "{message}");
let message = sole(ActionType::UnwrapKey, &json!({"expect_key": "763cbc"}));
assert!(
message.contains("sha256:") && message.contains("cmac-aes:"),
"{message}"
);
}
#[test]
fn rejects_an_unwrap_algorithm_outside_the_vocabulary() {
let message = sole(ActionType::UnwrapKey, &json!({"algorithm": "AES-192"}));
assert!(message.contains("unknown key algorithm"), "{message}");
}
#[test]
fn an_absent_field_is_deferred_not_missing() {
assert!(check(ActionType::GenerateKey, &json!({})).is_empty());
assert!(check(ActionType::IssueCertificate, &json!({})).is_empty());
}
#[test]
fn rejects_an_unknown_key_usage() {
let message = sole(
ActionType::GenerateKey,
&json!({"policy": {"usages": ["sign", "key_cert_sign"]}}),
);
assert!(message.contains("unknown key usage"), "{message}");
assert!(
message.contains("wrap"),
"the message should list what is available: {message}"
);
}
#[test]
fn accepts_the_pkcs11_usages() {
let with = json!({"policy": {"usages": ["sign", "verify", "wrap", "unwrap", "derive"]}});
assert!(check(ActionType::GenerateKey, &with).is_empty());
}
#[test]
fn a_non_string_value_is_rejected_whatever_it_would_have_meant() {
assert!(
sole(ActionType::GenerateKey, &json!({"algorithm": 4096})).contains("must be a string")
);
}
}