appcore_peer_rpc/
validation.rs1use super::*;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct PeerRpcValidationConfig {
16 pub local_tenant_id: TenantId,
18 pub local_cluster_id: ClusterId,
20 pub local_core_id: CoreId,
22 pub max_payload_bytes: usize,
24 pub nonce_window_ms: u64,
26}
27
28#[derive(Debug, Clone)]
30pub struct PeerRpcValidator {
31 config: PeerRpcValidationConfig,
32 local_protocol_version: ProtocolVersion,
33 nonce_store: Arc<dyn crate::PeerNonceStore>,
34}
35impl PeerRpcValidator {
36 pub fn new(config: PeerRpcValidationConfig) -> Self {
38 Self {
39 config,
40 local_protocol_version: ProtocolVersion::default(),
41 nonce_store: Arc::new(crate::InMemoryPeerNonceStore::default()),
42 }
43 }
44
45 pub fn with_protocol_version(mut self, protocol_version: ProtocolVersion) -> Self {
47 self.local_protocol_version = protocol_version;
48 self
49 }
50
51 pub fn with_nonce_store(mut self, nonce_store: Arc<dyn crate::PeerNonceStore>) -> Self {
53 self.nonce_store = nonce_store;
54 self
55 }
56
57 pub(crate) fn max_envelope_bytes(&self) -> usize {
58 self.config
59 .max_payload_bytes
60 .saturating_mul(4)
61 .saturating_add(MAX_ENVELOPE_OVERHEAD_BYTES)
62 }
63
64 pub fn validate(&self, envelope: &PeerRpcEnvelope, now_ms: u64) -> Result<(), PeerRpcError> {
66 validate_envelope_identifiers(envelope)?;
67 if envelope.payload.len() > self.config.max_payload_bytes {
68 return Err(PeerRpcError::PayloadTooLarge);
69 }
70 if envelope.tenant_id != self.config.local_tenant_id {
71 return Err(PeerRpcError::TenantMismatch);
72 }
73 if envelope.cluster_id != self.config.local_cluster_id {
74 return Err(PeerRpcError::ClusterMismatch);
75 }
76 if envelope.target_core_id != self.config.local_core_id {
77 return Err(PeerRpcError::TargetMismatch);
78 }
79 if !self
80 .local_protocol_version
81 .is_compatible_with(envelope.protocol_version)
82 {
83 return Err(PeerRpcError::ProtocolMismatch);
84 }
85 let window_ms = self.config.nonce_window_ms.max(1);
86 if envelope.timestamp_ms >= envelope.expires_at_ms
87 || envelope.expires_at_ms <= now_ms
88 || envelope.timestamp_ms > now_ms.saturating_add(window_ms)
89 || now_ms > envelope.timestamp_ms.saturating_add(window_ms)
90 {
91 return Err(PeerRpcError::Expired);
92 }
93 if envelope.body_hash != payload_hash(&envelope.payload) {
94 return Err(PeerRpcError::InvalidBodyHash);
95 }
96 if let Some(trace) = &envelope.trace {
97 if trace.trace_id != envelope.trace_id
98 || trace.tenant_id != envelope.tenant_id
99 || trace.current_core_id != envelope.source_core_id
100 {
101 return Err(PeerRpcError::InvalidEnvelope(
102 "trace_context_mismatch".to_string(),
103 ));
104 }
105 }
106 let nonce_expires_at_ms = envelope.expires_at_ms.min(now_ms.saturating_add(window_ms));
107 self.nonce_store
108 .check_and_record(&envelope.nonce, nonce_expires_at_ms, now_ms)?;
109 Ok(())
110 }
111}
112
113fn validate_envelope_identifiers(envelope: &PeerRpcEnvelope) -> Result<(), PeerRpcError> {
114 for (kind, value) in [
115 ("PeerRequestId", envelope.request_id.as_str()),
116 ("TraceId", envelope.trace_id.as_str()),
117 ("PeerNonce", envelope.nonce.as_str()),
118 ] {
119 validate_identifier(kind, value)
120 .map_err(|_| PeerRpcError::InvalidEnvelope("invalid_identifier".to_string()))?;
121 }
122 if let Some(idempotency_key) = &envelope.idempotency_key {
123 validate_identifier("IdempotencyKey", idempotency_key)
124 .map_err(|_| PeerRpcError::InvalidEnvelope("invalid_idempotency_key".to_string()))?;
125 }
126 envelope
127 .source_core_id
128 .validate()
129 .and_then(|_| envelope.target_core_id.validate())
130 .and_then(|_| envelope.tenant_id.validate())
131 .and_then(|_| envelope.cluster_id.validate())
132 .and_then(|_| envelope.capability.validate())
133 .map_err(|_| PeerRpcError::InvalidEnvelope("invalid_identifier".to_string()))
134}
135
136pub fn payload_hash(payload: &[u8]) -> String {
138 let digest = Sha256::digest(payload);
139 hex_encode(&digest)
140}
141
142pub fn envelope_signing_hash(envelope: &PeerRpcEnvelope) -> String {
144 let mut hasher = Sha256::new();
145 hash_field(&mut hasher, envelope.request_id.as_bytes());
146 hash_field(&mut hasher, envelope.trace_id.as_bytes());
147 hasher.update(envelope.protocol_version.as_u16().to_be_bytes());
148 hash_field(&mut hasher, envelope.source_core_id.as_str().as_bytes());
149 hash_field(&mut hasher, envelope.target_core_id.as_str().as_bytes());
150 hash_field(&mut hasher, envelope.tenant_id.as_str().as_bytes());
151 hash_field(&mut hasher, envelope.cluster_id.as_str().as_bytes());
152 hasher.update(envelope.timestamp_ms.to_be_bytes());
153 hasher.update(envelope.expires_at_ms.to_be_bytes());
154 hash_field(&mut hasher, envelope.nonce.as_bytes());
155 hash_field(&mut hasher, envelope.capability.as_str().as_bytes());
156 hash_field(&mut hasher, envelope.body_hash.as_bytes());
157 hash_optional_field(&mut hasher, envelope.idempotency_key.as_deref());
158 if let Some(trace) = &envelope.trace {
159 hasher.update([1]);
160 hash_field(&mut hasher, trace.trace_id.as_bytes());
161 hash_field(&mut hasher, trace.span_id.as_bytes());
162 hash_optional_field(&mut hasher, trace.parent_span_id.as_deref());
163 hash_field(&mut hasher, trace.originating_core_id.as_str().as_bytes());
164 hash_field(&mut hasher, trace.current_core_id.as_str().as_bytes());
165 hash_field(&mut hasher, trace.tenant_id.as_str().as_bytes());
166 hash_optional_field(&mut hasher, trace.command_id.as_deref());
167 } else {
168 hasher.update([0]);
169 }
170 hex_encode(&hasher.finalize())
171}
172
173fn hash_field(hasher: &mut Sha256, value: &[u8]) {
174 hasher.update((value.len() as u64).to_be_bytes());
175 hasher.update(value);
176}
177
178fn hash_optional_field(hasher: &mut Sha256, value: Option<&str>) {
179 if let Some(value) = value {
180 hasher.update([1]);
181 hash_field(hasher, value.as_bytes());
182 } else {
183 hasher.update([0]);
184 }
185}
186
187pub fn route_for_query() -> &'static str {
189 PEER_QUERY_PATH
190}
191
192pub fn route_for_command() -> &'static str {
194 PEER_COMMAND_PATH
195}
196
197fn hex_encode(bytes: &[u8]) -> String {
198 const HEX: &[u8; 16] = b"0123456789abcdef";
199 let mut out = String::with_capacity(bytes.len() * 2);
200 for byte in bytes {
201 out.push(HEX[(byte >> 4) as usize] as char);
202 out.push(HEX[(byte & 0x0f) as usize] as char);
203 }
204 out
205}