use super::*;
pub fn verify_record(
record: &Record,
prior: &[Record],
rules: &VerifierRules,
state: &State,
) -> VerdictData {
let index = PriorIndex::of(prior);
let prior = index.view(prior);
if let Some(reason) = check_record(record, &prior, rules, state) {
VerdictData {
result: VerdictResult::Reject,
reason: Some(reason),
}
} else {
VerdictData {
result: VerdictResult::Accept,
reason: None,
}
}
}
pub(super) fn check_record(
record: &Record,
prior: &Prior<'_>,
rules: &VerifierRules,
state: &State,
) -> Option<ReasonCode> {
if record.kind == Kind::Verdict {
return check_verdict_record(record, prior, rules);
}
let computed_id = match record.compute_id() {
Ok(id) => id,
Err(_) => return Some(ReasonCode::InvalidPayload),
};
if record.id != computed_id {
return Some(ReasonCode::InvalidPayload);
}
if !record
.refs
.windows(2)
.all(|w| (w[0].type_ as u8, &w[0].target) < (w[1].type_ as u8, &w[1].target))
{
return Some(ReasonCode::InvalidPayload);
}
if record.space != rules.space {
return Some(ReasonCode::InvalidPayload);
}
let expected_kind = match rules.kind_schema_map.get(&record.schema) {
Some(k) => *k,
None => return Some(ReasonCode::UnknownSchema),
};
if record.kind != expected_kind {
return Some(ReasonCode::KindSchemaMismatch);
}
if !allowed_author_types(record.kind).contains(&record.author.type_) {
return Some(ReasonCode::AuthorRoleInvalid);
}
match rules.author_roles.get(&record.author.id) {
Some(role) => {
if record.author.type_ != *role {
return Some(ReasonCode::AuthorRoleInvalid);
}
}
None => {
if requires_registered_author(record.kind) {
return Some(ReasonCode::AuthorRoleInvalid);
}
}
}
let sig_required = rules.signature_required_kinds.contains(&record.kind)
|| rules.author_keys.contains_key(&record.author.id);
if sig_required && record.author.signature.is_none() {
return Some(ReasonCode::SignatureMissing);
}
if record.author.signature.is_some() {
match crate::record::sign::verified_key(record) {
None => return Some(ReasonCode::SignatureInvalid),
Some(key) => {
if let Some(allowed) = rules.author_keys.get(&record.author.id) {
if !allowed.contains(&key) {
return Some(ReasonCode::SignatureInvalid);
}
}
}
}
}
for r in &record.refs {
let target = prior.find(r.target);
match target {
None => return Some(ReasonCode::RefUnresolved),
Some(t) => {
if t.space != record.space {
return Some(ReasonCode::RefCrossSpace);
}
if r.type_ == RefType::Require
&& (!state.accepted_records.contains(&t.id)
|| state.retracted_records.contains(&t.id)
|| state.tainted_records.contains(&t.id))
{
return Some(ReasonCode::RefUnresolved);
}
}
}
}
let replace_refs: Vec<&Ref> = record
.refs
.iter()
.filter(|r| r.type_ == RefType::Replace)
.collect();
if replace_refs.len() > 1 {
return Some(ReasonCode::InvalidPayload);
}
if let Some(replace_ref) = replace_refs.first() {
match record.kind {
Kind::Summary | Kind::Capability | Kind::Approval | Kind::Plan => {}
_ => return Some(ReasonCode::ReplacementInvalid),
}
let target = prior.find(replace_ref.target);
match target {
None => return Some(ReasonCode::ReplacementInvalid),
Some(t) => {
if !state.accepted_records.contains(&t.id) {
return Some(ReasonCode::ReplacementInvalid);
}
if t.kind != record.kind {
return Some(ReasonCode::ReplacementInvalid);
}
if let Some(reason) = check_replacement_compatibility(record, t) {
return Some(reason);
}
}
}
}
let ref_evidences: Vec<Evidence> = record
.refs
.iter()
.filter_map(|r| prior.find(r.target).and_then(|t| state.ref_evidence(r, t)))
.collect();
let expected_evidence = derive_evidence(&record.schema, &ref_evidences);
if record.evidence != expected_evidence {
return Some(ReasonCode::InvalidPayload);
}
if let Some(threshold) = rules.evidence_thresholds.get(&record.kind) {
if expected_evidence > *threshold {
return Some(ReasonCode::EvidenceBelowThreshold);
}
}
if let Some(reason) = check_payload_decode(record) {
return Some(reason);
}
check_kind_specific(record, prior, rules, state)
}
fn check_replacement_compatibility(record: &Record, target: &Record) -> Option<ReasonCode> {
match record.kind {
Kind::Capability => {
let new_data: CapabilityData = dec!(&record.data, CapabilityData);
let old_data: CapabilityData = dec!(&target.data, CapabilityData);
if new_data.actor_id != old_data.actor_id
|| new_data.action_class != old_data.action_class
|| new_data.scope != old_data.scope
{
return Some(ReasonCode::ReplacementInvalid);
}
}
Kind::Approval => {
let new_data: ApprovalData = dec!(&record.data, ApprovalData);
let old_data: ApprovalData = dec!(&target.data, ApprovalData);
if new_data.target_action.is_some() && old_data.target_action.is_some() {
if new_data.scope != old_data.scope
|| new_data.target_action != old_data.target_action
{
return Some(ReasonCode::ReplacementInvalid);
}
} else if new_data.action_class.is_some() && old_data.action_class.is_some() {
if new_data.action_class != old_data.action_class
|| new_data.scope != old_data.scope
|| new_data.actor_id != old_data.actor_id
{
return Some(ReasonCode::ReplacementInvalid);
}
} else {
return Some(ReasonCode::ReplacementInvalid);
}
}
Kind::Summary => {
let new_data: SummaryData = dec!(&record.data, SummaryData);
let old_data: SummaryData = dec!(&target.data, SummaryData);
if new_data.summary_type != old_data.summary_type
|| new_data.subject != old_data.subject
|| new_data.scope != old_data.scope
{
return Some(ReasonCode::ReplacementInvalid);
}
}
Kind::Plan => {
let new_data: PlanData = dec!(&record.data, PlanData);
let old_data: PlanData = dec!(&target.data, PlanData);
if new_data.request_id != old_data.request_id {
return Some(ReasonCode::ReplacementInvalid);
}
}
_ => {}
}
None
}
pub(super) fn decodes_canonically<T: serde::de::DeserializeOwned + serde::Serialize>(
data: &[u8],
) -> bool {
match serde_json::from_slice::<T>(data) {
Ok(value) => match crate::base::canonical::canonical_json(&value) {
Ok(reencoded) => reencoded == data,
Err(_) => false,
},
Err(_) => false,
}
}
fn check_payload_decode(record: &Record) -> Option<ReasonCode> {
let schema = record.schema;
let ok = match () {
_ if schema == schema_id(SCHEMA_REQUEST) => {
decodes_canonically::<RequestData>(&record.data)
}
_ if schema == schema_id(SCHEMA_ACTION) => decodes_canonically::<ActionData>(&record.data),
_ if schema == schema_id(SCHEMA_RESPONSE) => {
decodes_canonically::<ResponseData>(&record.data)
}
_ if schema == schema_id(SCHEMA_RESULT)
|| schema == schema_id(SCHEMA_RESULT_EXTERNAL)
|| schema == schema_id(SCHEMA_RESULT_EFFECT_CONFIRMATION) =>
{
decodes_canonically::<ResultData>(&record.data)
}
_ if schema == schema_id(SCHEMA_CAPABILITY) => {
decodes_canonically::<CapabilityData>(&record.data)
}
_ if schema == schema_id(SCHEMA_APPROVAL) => {
decodes_canonically::<ApprovalData>(&record.data)
}
_ if schema == schema_id(SCHEMA_SUMMARY) => {
decodes_canonically::<SummaryData>(&record.data)
}
_ if schema == schema_id(SCHEMA_REFUSAL) => {
decodes_canonically::<RefusalData>(&record.data)
}
_ if schema == schema_id(SCHEMA_USAGE) => decodes_canonically::<UsageData>(&record.data),
_ if schema == schema_id(SCHEMA_VERDICT) => {
decodes_canonically::<VerdictData>(&record.data)
}
_ if schema == schema_id(SCHEMA_PLAN) => decodes_canonically::<PlanData>(&record.data),
_ if schema == schema_id(SCHEMA_RETRACTION) => {
decodes_canonically::<RetractionData>(&record.data)
}
_ => return Some(ReasonCode::UnknownSchema),
};
if ok {
None
} else {
Some(ReasonCode::InvalidPayload)
}
}