use std::fmt;
use std::sync::atomic::Ordering;
use std::time::Duration;
use super::super::org::OrgMembershipCert;
use super::evaluator::{validate_interest_constraints, SensingCounters};
use super::identity::{
AudienceScopeCommitment, CanonicalConstraints, CapabilityId, ConstraintError, Digest256,
DisclosureClass, InterestSpec, ProviderSelector, ResultMode, WorkLatencyEnvelope,
};
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum SensingInterestFrame {
CapabilityRegistration {
capability_id: CapabilityId,
constraints: Vec<u8>,
constraints_digest: Digest256,
work_latency: WorkLatencyEnvelope,
providers: ProviderSelector,
result_mode: ResultMode,
interest_digest: Digest256,
requested_sample_interval: Duration,
soft_state_ttl: Duration,
audience_scope: AudienceScopeCommitment,
consumer: u64,
},
ProviderRegistration {
target: u64,
capability_id: CapabilityId,
constraints: Vec<u8>,
constraints_digest: Digest256,
work_latency: WorkLatencyEnvelope,
providers: ProviderSelector,
result_mode: ResultMode,
disclosure_class: DisclosureClass,
audience_scope: AudienceScopeCommitment,
interest_digest: Digest256,
requested_sample_interval: Duration,
soft_state_ttl: Duration,
},
Deregister {
interest_digest: Digest256,
target: Option<u64>,
},
OrgCapabilityRegistration {
capability_id: CapabilityId,
constraints: Vec<u8>,
constraints_digest: Digest256,
work_latency: WorkLatencyEnvelope,
providers: ProviderSelector,
result_mode: ResultMode,
interest_digest: Digest256,
requested_sample_interval: Duration,
soft_state_ttl: Duration,
audience_scope: AudienceScopeCommitment,
consumer: u64,
subscriber_membership: OrgMembershipCert,
},
OrgProviderRegistration {
target: u64,
capability_id: CapabilityId,
constraints: Vec<u8>,
constraints_digest: Digest256,
work_latency: WorkLatencyEnvelope,
providers: ProviderSelector,
result_mode: ResultMode,
disclosure_class: DisclosureClass,
audience_scope: AudienceScopeCommitment,
interest_digest: Digest256,
requested_sample_interval: Duration,
soft_state_ttl: Duration,
subscriber_membership: OrgMembershipCert,
},
}
impl SensingInterestFrame {
pub fn capability_registration(
spec: &InterestSpec,
requested_sample_interval: Duration,
soft_state_ttl: Duration,
consumer: u64,
) -> Self {
Self::CapabilityRegistration {
capability_id: spec.capability_id.clone(),
constraints: spec.constraints.canonical_bytes(),
constraints_digest: spec.constraints.constraints_digest(),
work_latency: spec.work_latency,
providers: spec.providers.clone(),
result_mode: spec.result_mode,
interest_digest: spec.interest_digest(),
requested_sample_interval,
soft_state_ttl,
audience_scope: spec.audience,
consumer,
}
}
pub fn provider_registration(
spec: &InterestSpec,
target: u64,
requested_sample_interval: Duration,
soft_state_ttl: Duration,
) -> Self {
Self::ProviderRegistration {
target,
capability_id: spec.capability_id.clone(),
constraints: spec.constraints.canonical_bytes(),
constraints_digest: spec.constraints.constraints_digest(),
work_latency: spec.work_latency,
providers: spec.providers.clone(),
result_mode: spec.result_mode,
disclosure_class: spec.disclosure_class,
audience_scope: spec.audience,
interest_digest: spec.interest_digest(),
requested_sample_interval,
soft_state_ttl,
}
}
pub fn org_capability_registration(
spec: &InterestSpec,
requested_sample_interval: Duration,
soft_state_ttl: Duration,
consumer: u64,
subscriber_membership: OrgMembershipCert,
) -> Self {
Self::OrgCapabilityRegistration {
capability_id: spec.capability_id.clone(),
constraints: spec.constraints.canonical_bytes(),
constraints_digest: spec.constraints.constraints_digest(),
work_latency: spec.work_latency,
providers: spec.providers.clone(),
result_mode: spec.result_mode,
interest_digest: spec.interest_digest(),
requested_sample_interval,
soft_state_ttl,
audience_scope: spec.audience,
consumer,
subscriber_membership,
}
}
pub fn org_provider_registration(
spec: &InterestSpec,
target: u64,
requested_sample_interval: Duration,
soft_state_ttl: Duration,
subscriber_membership: OrgMembershipCert,
) -> Self {
Self::OrgProviderRegistration {
target,
capability_id: spec.capability_id.clone(),
constraints: spec.constraints.canonical_bytes(),
constraints_digest: spec.constraints.constraints_digest(),
work_latency: spec.work_latency,
providers: spec.providers.clone(),
result_mode: spec.result_mode,
disclosure_class: spec.disclosure_class,
audience_scope: spec.audience,
interest_digest: spec.interest_digest(),
requested_sample_interval,
soft_state_ttl,
subscriber_membership,
}
}
pub fn reconstruct_spec(&self, constraints: CanonicalConstraints) -> Option<InterestSpec> {
match self {
Self::CapabilityRegistration {
capability_id,
work_latency,
providers,
result_mode,
audience_scope,
..
} => Some(InterestSpec {
capability_id: capability_id.clone(),
constraints,
work_latency: *work_latency,
providers: providers.clone(),
result_mode: *result_mode,
disclosure_class: DisclosureClass::Owner,
audience: *audience_scope,
}),
Self::ProviderRegistration {
capability_id,
work_latency,
providers,
result_mode,
disclosure_class,
audience_scope,
..
}
| Self::OrgProviderRegistration {
capability_id,
work_latency,
providers,
result_mode,
disclosure_class,
audience_scope,
..
} => Some(InterestSpec {
capability_id: capability_id.clone(),
constraints,
work_latency: *work_latency,
providers: providers.clone(),
result_mode: *result_mode,
disclosure_class: *disclosure_class,
audience: *audience_scope,
}),
Self::OrgCapabilityRegistration {
capability_id,
work_latency,
providers,
result_mode,
audience_scope,
..
} => Some(InterestSpec {
capability_id: capability_id.clone(),
constraints,
work_latency: *work_latency,
providers: providers.clone(),
result_mode: *result_mode,
disclosure_class: DisclosureClass::Owner,
audience: *audience_scope,
}),
Self::Deregister { .. } => None,
}
}
pub fn validated_spec(
&self,
counters: &SensingCounters,
) -> Result<InterestSpec, FrameSpecError> {
let (constraint_bytes, constraints_digest, claimed_digest) = match self {
Self::CapabilityRegistration {
constraints,
constraints_digest,
interest_digest,
..
}
| Self::ProviderRegistration {
constraints,
constraints_digest,
interest_digest,
..
}
| Self::OrgCapabilityRegistration {
constraints,
constraints_digest,
interest_digest,
..
}
| Self::OrgProviderRegistration {
constraints,
constraints_digest,
interest_digest,
..
} => (constraints, constraints_digest, interest_digest),
Self::Deregister { .. } => return Err(FrameSpecError::NotARegistration),
};
let constraints =
validate_interest_constraints(constraint_bytes, constraints_digest, counters)
.map_err(FrameSpecError::Constraints)?;
let spec = self
.reconstruct_spec(constraints)
.ok_or(FrameSpecError::NotARegistration)?;
if spec.interest_digest() != *claimed_digest {
counters.protocol_invalid.fetch_add(1, Ordering::Relaxed);
return Err(FrameSpecError::InterestDigestMismatch);
}
Ok(spec)
}
pub fn validate_provider_registration(
&self,
counters: &SensingCounters,
) -> Result<ValidatedProviderRegistration, FrameSpecError> {
let Self::ProviderRegistration {
target,
requested_sample_interval,
soft_state_ttl,
..
} = self
else {
return Err(FrameSpecError::NotProviderAddressed);
};
let spec = self.validated_spec(counters)?;
Ok(ValidatedProviderRegistration {
target: *target,
spec,
requested_sample_interval: *requested_sample_interval,
soft_state_ttl: *soft_state_ttl,
})
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ValidatedProviderRegistration {
pub target: u64,
pub spec: InterestSpec,
pub requested_sample_interval: Duration,
pub soft_state_ttl: Duration,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FrameSpecError {
NotARegistration,
NotProviderAddressed,
Constraints(ConstraintError),
InterestDigestMismatch,
}
impl FrameSpecError {
pub const fn is_security_relevant(self) -> bool {
match self {
Self::InterestDigestMismatch => true,
Self::Constraints(error) => error.is_security_relevant(),
Self::NotARegistration | Self::NotProviderAddressed => false,
}
}
}
impl fmt::Display for FrameSpecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotARegistration => f.write_str("deregister frames carry no interest spec"),
Self::NotProviderAddressed => {
f.write_str("frame is not a provider-addressed ProviderRegistration")
}
Self::Constraints(error) => write!(f, "constraint intake refused: {error}"),
Self::InterestDigestMismatch => {
f.write_str("re-derived interest digest does not match the frame's claim")
}
}
}
}
impl std::error::Error for FrameSpecError {}
#[cfg(test)]
mod tests {
use super::super::identity::{CanonicalConstraints, DisclosureClass};
use super::*;
fn spec() -> InterestSpec {
InterestSpec {
capability_id: CapabilityId::new("print.document"),
constraints: CanonicalConstraints::from_entries([("color", "true"), ("media", "a4")])
.unwrap(),
work_latency: WorkLatencyEnvelope::start_within(Duration::from_secs(5)),
providers: ProviderSelector::AnyAuthorized,
result_mode: ResultMode::Any,
disclosure_class: DisclosureClass::Owner,
audience: AudienceScopeCommitment::from_bytes([0xAA; 32]),
}
}
const CAP_HEX: &str = "000e7072696e742e646f63756d656e74240200000005000000636f6c6f720400000074727565050000006d6564696102000000613420d02d423654096a867b66a506b433528db701e41818066eec51e186c3724be398010500000000204f9d6f145f2df01fa70c8155e7e9c55fe5571d6df47b631749ce35edc0b250fd0080c2d72f1e0020aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacea328";
const PROV_HEX: &str = "01770e7072696e742e646f63756d656e74240200000005000000636f6c6f720400000074727565050000006d6564696102000000613420d02d423654096a867b66a506b433528db701e41818066eec51e186c3724be3980105000000000020aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa204f9d6f145f2df01fa70c8155e7e9c55fe5571d6df47b631749ce35edc0b250fd0080c2d72f1e00";
const DEREG_HEX: &str =
"0220bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb019901";
fn golden_dereg() -> SensingInterestFrame {
SensingInterestFrame::Deregister {
interest_digest: Digest256::from_bytes([0xBB; 32]),
target: Some(0x99),
}
}
#[test]
fn existing_variants_have_frozen_postcard_encodings() {
use crate::adapter::net::behavior::sensing::encode_interest_frame;
let cap = SensingInterestFrame::capability_registration(
&spec(),
Duration::from_millis(100),
Duration::from_secs(30),
0xA11CE,
);
let prov = SensingInterestFrame::provider_registration(
&spec(),
0x77,
Duration::from_millis(100),
Duration::from_secs(30),
);
let dereg = golden_dereg();
assert_eq!(hex::encode(encode_interest_frame(&cap).unwrap()), CAP_HEX);
assert_eq!(hex::encode(encode_interest_frame(&prov).unwrap()), PROV_HEX);
assert_eq!(
hex::encode(encode_interest_frame(&dereg).unwrap()),
DEREG_HEX
);
assert_eq!(encode_interest_frame(&cap).unwrap()[0], 0);
assert_eq!(encode_interest_frame(&prov).unwrap()[0], 1);
assert_eq!(encode_interest_frame(&dereg).unwrap()[0], 2);
}
fn cert() -> OrgMembershipCert {
OrgMembershipCert::try_issue(
&crate::adapter::net::behavior::org::OrgKeypair::from_bytes([0x42u8; 32]),
crate::adapter::net::identity::EntityId::from_bytes([0x24u8; 32]),
5,
crate::adapter::net::behavior::org::ORG_CERT_TTL_SECS_RECOMMENDED,
)
.expect("issue cert")
}
fn org_cap_frame() -> SensingInterestFrame {
SensingInterestFrame::org_capability_registration(
&spec(),
Duration::from_millis(100),
Duration::from_secs(30),
0xA11CE,
cert(),
)
}
fn org_prov_frame() -> SensingInterestFrame {
SensingInterestFrame::org_provider_registration(
&spec(),
0x77,
Duration::from_millis(100),
Duration::from_secs(30),
cert(),
)
}
#[test]
fn org_variants_land_at_postcard_indices_3_and_4() {
use crate::adapter::net::behavior::sensing::encode_interest_frame;
assert_eq!(encode_interest_frame(&org_cap_frame()).unwrap()[0], 3);
assert_eq!(encode_interest_frame(&org_prov_frame()).unwrap()[0], 4);
}
#[test]
fn org_frames_round_trip_and_preserve_the_embedded_cert() {
use crate::adapter::net::behavior::sensing::{
decode_interest_frame, encode_interest_frame,
};
for frame in [org_cap_frame(), org_prov_frame()] {
let bytes = encode_interest_frame(&frame).unwrap();
let back = decode_interest_frame(&bytes).expect("strict decode");
assert_eq!(back, frame);
}
}
#[test]
fn a_truncated_embedded_cert_fails_frame_decode() {
use crate::adapter::net::behavior::sensing::{
decode_interest_frame, encode_interest_frame,
};
let mut bytes = encode_interest_frame(&org_cap_frame()).unwrap();
bytes.truncate(bytes.len() - 10);
assert!(decode_interest_frame(&bytes).is_err());
}
#[test]
fn org_frame_trailing_bytes_fail_strict_decode() {
use crate::adapter::net::behavior::sensing::{
decode_interest_frame, encode_interest_frame,
};
let mut bytes = encode_interest_frame(&org_prov_frame()).unwrap();
bytes.push(0x00);
assert!(decode_interest_frame(&bytes).is_err());
}
#[test]
fn validated_spec_reconstructs_org_variants() {
let counters = SensingCounters::default();
assert_eq!(org_cap_frame().validated_spec(&counters).unwrap(), spec());
assert_eq!(org_prov_frame().validated_spec(&counters).unwrap(), spec());
}
#[test]
fn capability_registration_round_trips_through_json() {
let frame = SensingInterestFrame::capability_registration(
&spec(),
Duration::from_millis(100),
Duration::from_secs(30),
0xA11CE,
);
let json = serde_json::to_string(&frame).unwrap();
let back: SensingInterestFrame = serde_json::from_str(&json).unwrap();
assert_eq!(back, frame);
}
#[test]
fn provider_registration_round_trips_and_carries_population_fields() {
let frame = SensingInterestFrame::provider_registration(
&spec(),
0x77,
Duration::from_millis(100),
Duration::from_secs(30),
);
let json = serde_json::to_value(&frame).unwrap();
let body = &json["ProviderRegistration"];
assert!(body.is_object());
assert!(body.get("providers").is_some());
assert!(body.get("result_mode").is_some());
assert!(body.get("disclosure_class").is_some());
assert!(body.get("consumer_budget").is_none());
let back: SensingInterestFrame = serde_json::from_value(json).unwrap();
assert_eq!(back, frame);
}
#[test]
fn validated_spec_reconstructs_the_complete_spec_on_both_legs() {
let spec = spec();
let counters = SensingCounters::default();
let leader_leg = SensingInterestFrame::capability_registration(
&spec,
Duration::from_millis(100),
Duration::from_secs(30),
0xA,
);
let provider_leg = SensingInterestFrame::provider_registration(
&spec,
0x77,
Duration::from_millis(100),
Duration::from_secs(30),
);
for frame in [&leader_leg, &provider_leg] {
let validated = frame.validated_spec(&counters).unwrap();
assert_eq!(validated, spec);
assert_eq!(validated.interest_digest(), spec.interest_digest());
}
assert_eq!(SensingCounters::get(&counters.invalid_constraints), 0);
assert_eq!(SensingCounters::get(&counters.protocol_invalid), 0);
}
#[test]
fn validate_provider_registration_returns_the_branch_parameters() {
let spec = spec();
let counters = SensingCounters::default();
let frame = SensingInterestFrame::provider_registration(
&spec,
0x77,
Duration::from_millis(100),
Duration::from_secs(30),
);
let validated = frame.validate_provider_registration(&counters).unwrap();
assert_eq!(validated.target, 0x77);
assert_eq!(validated.spec, spec);
assert_eq!(
validated.requested_sample_interval,
Duration::from_millis(100)
);
assert_eq!(validated.soft_state_ttl, Duration::from_secs(30));
let leader_leg = SensingInterestFrame::capability_registration(
&spec,
Duration::from_millis(100),
Duration::from_secs(30),
0xA,
);
assert_eq!(
leader_leg.validate_provider_registration(&counters),
Err(FrameSpecError::NotProviderAddressed),
);
assert_eq!(SensingCounters::get(&counters.protocol_invalid), 0);
}
#[test]
fn tampered_population_fields_fail_provider_digest_validation() {
let base = || {
SensingInterestFrame::provider_registration(
&spec(),
0x77,
Duration::from_millis(100),
Duration::from_secs(30),
)
};
type FrameMutation = fn(&mut SensingInterestFrame);
let mutations: [(&str, FrameMutation); 3] = [
("providers", |frame| {
let SensingInterestFrame::ProviderRegistration { providers, .. } = frame else {
panic!("helper builds the provider leg");
};
*providers = ProviderSelector::Node(0x77);
}),
("result_mode", |frame| {
let SensingInterestFrame::ProviderRegistration { result_mode, .. } = frame else {
panic!("helper builds the provider leg");
};
*result_mode = ResultMode::Each;
}),
("work_latency", |frame| {
let SensingInterestFrame::ProviderRegistration { work_latency, .. } = frame else {
panic!("helper builds the provider leg");
};
*work_latency = WorkLatencyEnvelope::start_within(Duration::from_secs(6));
}),
];
for (field, mutate) in mutations {
let counters = SensingCounters::default();
let mut frame = base();
mutate(&mut frame);
let rejection = frame.validate_provider_registration(&counters).unwrap_err();
assert_eq!(
rejection,
FrameSpecError::InterestDigestMismatch,
"tampered {field} must fail digest re-derivation",
);
assert!(rejection.is_security_relevant());
assert_eq!(SensingCounters::get(&counters.protocol_invalid), 1);
assert_eq!(SensingCounters::get(&counters.invalid_constraints), 0);
}
}
#[test]
fn corrupted_constraints_fail_intake_before_digest_re_derivation() {
let counters = SensingCounters::default();
let mut frame = SensingInterestFrame::provider_registration(
&spec(),
0x77,
Duration::from_millis(100),
Duration::from_secs(30),
);
let SensingInterestFrame::ProviderRegistration { constraints, .. } = &mut frame else {
panic!("helper builds the provider leg");
};
constraints[0] ^= 1;
let rejection = frame.validated_spec(&counters).unwrap_err();
assert!(matches!(rejection, FrameSpecError::Constraints(_)));
assert_eq!(SensingCounters::get(&counters.invalid_constraints), 1);
}
#[test]
fn deregister_carries_no_spec() {
let counters = SensingCounters::default();
let frame = SensingInterestFrame::Deregister {
interest_digest: spec().interest_digest(),
target: None,
};
assert_eq!(
frame.validated_spec(&counters),
Err(FrameSpecError::NotARegistration),
);
assert_eq!(
frame.validate_provider_registration(&counters),
Err(FrameSpecError::NotProviderAddressed),
);
assert!(!FrameSpecError::NotARegistration.is_security_relevant());
assert_eq!(SensingCounters::get(&counters.protocol_invalid), 0);
}
#[test]
fn deregister_round_trips_both_addressing_modes() {
for target in [None, Some(0x77u64)] {
let frame = SensingInterestFrame::Deregister {
interest_digest: spec().interest_digest(),
target,
};
let json = serde_json::to_string(&frame).unwrap();
let back: SensingInterestFrame = serde_json::from_str(&json).unwrap();
assert_eq!(back, frame);
}
}
#[test]
fn helper_builds_internally_consistent_frames() {
let spec = spec();
let frame = SensingInterestFrame::capability_registration(
&spec,
Duration::from_millis(100),
Duration::from_secs(30),
0xA,
);
let SensingInterestFrame::CapabilityRegistration {
constraints,
constraints_digest,
interest_digest,
audience_scope,
..
} = &frame
else {
panic!("helper must build the leader-addressed variant");
};
let parsed = CanonicalConstraints::validate_inline(constraints, constraints_digest)
.expect("inline bytes must match the carried digest");
assert_eq!(parsed, spec.constraints);
assert_eq!(*interest_digest, spec.interest_digest());
assert_eq!(*audience_scope, spec.audience);
}
#[test]
fn digest_fields_serialize_as_hex_strings() {
let frame = SensingInterestFrame::Deregister {
interest_digest: Digest256::from_bytes([0x0F; 32]),
target: None,
};
let json = serde_json::to_value(&frame).unwrap();
assert_eq!(
json["Deregister"]["interest_digest"],
serde_json::Value::String("0f".repeat(32)),
);
}
}