1use std::collections::BTreeSet;
2
3use chio_core_types::canonical::canonical_json_bytes_from_str;
4use chio_core_types::crypto::{sha256_hex, Keypair};
5use chio_core_types::receipt::lineage::SignedExportEnvelope;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use super::{
10 canonical_outcome_bytes, domain_digest, envelope_digest, load_canonical_outcome_json,
11 validate_current_window, validate_digest, validate_text, validate_window, OutcomeError,
12 OutcomeSignerTrustV1,
13};
14
15pub const OUTCOME_PREDICATE_SCHEMA: &str = chio_core_types::CHIO_OUTCOME_PREDICATE_V1_SCHEMA;
16
17const PREDICATE_ID_DOMAIN: &[u8] = b"chio.outcome.predicate.id.v1\0";
18const PREDICATE_BODY_DIGEST_DOMAIN: &[u8] = b"chio.outcome.predicate.body.v1\0";
19const MAX_ASSERTIONS: usize = 256;
20const MAX_POINTER_CHARS: usize = 2_048;
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
24pub enum OutcomeComparatorV1 {
25 Exists,
26 Eq { value: Value },
27 Ne { value: Value },
28 Lt { value: Value },
29 Lte { value: Value },
30 Gt { value: Value },
31 Gte { value: Value },
32}
33
34impl OutcomeComparatorV1 {
35 fn validate(&self) -> Result<(), OutcomeError> {
36 match self {
37 Self::Exists | Self::Eq { .. } | Self::Ne { .. } => Ok(()),
38 Self::Lt { value } | Self::Lte { value } | Self::Gt { value } | Self::Gte { value } => {
39 integer(value)
40 .map(|_| ())
41 .ok_or(OutcomeError::InvalidField("ordered_comparator_value"))
42 }
43 }
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase", deny_unknown_fields)]
49pub struct OutcomeAssertionV1 {
50 pub pointer: String,
51 pub comparator: OutcomeComparatorV1,
52}
53
54impl OutcomeAssertionV1 {
55 fn validate(&self) -> Result<(), OutcomeError> {
56 validate_pointer(&self.pointer)?;
57 self.comparator.validate()
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct OutcomePredicateInputV1 {
63 pub assertions: Vec<OutcomeAssertionV1>,
64 pub provider_id: String,
65 pub issued_at_unix_ms: u64,
66 pub expires_at_unix_ms: u64,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase", deny_unknown_fields)]
71pub struct OutcomePredicateBodyV1 {
72 schema: String,
73 predicate_id: String,
74 assertions: Vec<OutcomeAssertionV1>,
75 provider_id: String,
76 issued_at_unix_ms: u64,
77 expires_at_unix_ms: u64,
78}
79
80#[derive(Serialize)]
81#[serde(rename_all = "camelCase")]
82struct PredicateIdPreimage<'a> {
83 schema: &'a str,
84 assertions: &'a [OutcomeAssertionV1],
85 provider_id: &'a str,
86 issued_at_unix_ms: u64,
87 expires_at_unix_ms: u64,
88}
89
90impl OutcomePredicateBodyV1 {
91 pub fn new(input: OutcomePredicateInputV1) -> Result<Self, OutcomeError> {
92 let mut body = Self {
93 schema: OUTCOME_PREDICATE_SCHEMA.to_owned(),
94 predicate_id: String::new(),
95 assertions: input.assertions,
96 provider_id: input.provider_id,
97 issued_at_unix_ms: input.issued_at_unix_ms,
98 expires_at_unix_ms: input.expires_at_unix_ms,
99 };
100 body.predicate_id = body.derived_id()?;
101 body.validate()?;
102 Ok(body)
103 }
104
105 pub fn validate(&self) -> Result<(), OutcomeError> {
106 if self.schema != OUTCOME_PREDICATE_SCHEMA {
107 return Err(OutcomeError::InvalidField("predicate_schema"));
108 }
109 validate_digest("predicate_id", &self.predicate_id)?;
110 validate_text("provider_id", &self.provider_id)?;
111 validate_window(self.issued_at_unix_ms, self.expires_at_unix_ms)?;
112 if self.assertions.is_empty() || self.assertions.len() > MAX_ASSERTIONS {
113 return Err(OutcomeError::InvalidField("assertions"));
114 }
115 let mut unique = BTreeSet::new();
116 for assertion in &self.assertions {
117 assertion.validate()?;
118 if !unique.insert(canonical_outcome_bytes(assertion)?) {
119 return Err(OutcomeError::InvalidField("duplicate_assertion"));
120 }
121 }
122 if self.predicate_id != self.derived_id()? {
123 return Err(OutcomeError::BindingMismatch);
124 }
125 Ok(())
126 }
127
128 fn derived_id(&self) -> Result<String, OutcomeError> {
129 domain_digest(
130 PREDICATE_ID_DOMAIN,
131 &PredicateIdPreimage {
132 schema: &self.schema,
133 assertions: &self.assertions,
134 provider_id: &self.provider_id,
135 issued_at_unix_ms: self.issued_at_unix_ms,
136 expires_at_unix_ms: self.expires_at_unix_ms,
137 },
138 )
139 }
140
141 #[must_use]
142 pub fn predicate_id(&self) -> &str {
143 &self.predicate_id
144 }
145
146 #[must_use]
147 pub fn provider_id(&self) -> &str {
148 &self.provider_id
149 }
150
151 #[must_use]
152 pub fn assertions(&self) -> &[OutcomeAssertionV1] {
153 &self.assertions
154 }
155
156 #[must_use]
157 pub const fn expires_at_unix_ms(&self) -> u64 {
158 self.expires_at_unix_ms
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(transparent)]
164pub struct SignedOutcomePredicateV1(SignedExportEnvelope<OutcomePredicateBodyV1>);
165
166impl SignedOutcomePredicateV1 {
167 pub fn sign(body: OutcomePredicateBodyV1, signer: &Keypair) -> Result<Self, OutcomeError> {
168 body.validate()?;
169 SignedExportEnvelope::sign(body, signer)
170 .map(Self)
171 .map_err(|error| OutcomeError::Canonicalization(error.to_string()))
172 }
173
174 #[must_use]
175 pub const fn body(&self) -> &OutcomePredicateBodyV1 {
176 &self.0.body
177 }
178}
179
180pub struct OutcomePredicateVerificationV1<'a> {
181 pub provider_id: &'a str,
182 pub trust: &'a OutcomeSignerTrustV1,
183 pub trusted_now_unix_ms: u64,
184}
185
186#[derive(Debug, Clone)]
187pub struct VerifiedOutcomePredicateV1 {
188 signed: SignedOutcomePredicateV1,
189 body_digest: String,
190 envelope_digest: String,
191}
192
193impl VerifiedOutcomePredicateV1 {
194 #[must_use]
195 pub const fn body(&self) -> &OutcomePredicateBodyV1 {
196 self.signed.body()
197 }
198
199 #[must_use]
200 pub fn body_digest(&self) -> &str {
201 &self.body_digest
202 }
203
204 #[must_use]
205 pub fn envelope_digest(&self) -> &str {
206 &self.envelope_digest
207 }
208}
209
210pub fn verify_outcome_predicate(
211 canonical_envelope: &[u8],
212 context: &OutcomePredicateVerificationV1<'_>,
213) -> Result<VerifiedOutcomePredicateV1, OutcomeError> {
214 let signed: SignedOutcomePredicateV1 = load_canonical_outcome_json(canonical_envelope)?;
215 signed.body().validate()?;
216 if signed.body().provider_id != context.provider_id
217 || signed.body().provider_id != context.trust.principal_id()
218 {
219 return Err(OutcomeError::BindingMismatch);
220 }
221 if signed.0.signer_key != *context.trust.key()
222 || !signed
223 .0
224 .verify_signature()
225 .map_err(|error| OutcomeError::Canonicalization(error.to_string()))?
226 {
227 return Err(OutcomeError::AuthorityVerification);
228 }
229 validate_current_window(
230 signed.body().issued_at_unix_ms,
231 signed.body().expires_at_unix_ms,
232 context.trust.max_lifetime_ms(),
233 context.trusted_now_unix_ms,
234 )?;
235 Ok(VerifiedOutcomePredicateV1 {
236 body_digest: domain_digest(PREDICATE_BODY_DIGEST_DOMAIN, signed.body())?,
237 envelope_digest: envelope_digest(&signed)?,
238 signed,
239 })
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243#[serde(rename_all = "snake_case")]
244pub enum OutcomeEvaluationReasonV1 {
245 AssertionMismatch,
246 MissingTarget,
247 TargetNotInteger,
248 InvalidOutputJson,
249 DeliveryCancelled,
250 OutputBlocked,
251 OutputMutationAfterEvaluation,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255#[serde(tag = "verdict", rename_all = "snake_case", deny_unknown_fields)]
256pub enum OutcomeEvaluationV1 {
257 Passed,
258 Failed {
259 assertion_index: u32,
260 reason: OutcomeEvaluationReasonV1,
261 },
262 Unevaluable {
263 reason: OutcomeEvaluationReasonV1,
264 },
265}
266
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct VerifiedOutcomeEvaluationV1 {
269 evaluation: OutcomeEvaluationV1,
270 output_digest: String,
271 predicate_id: String,
272 predicate_digest: String,
273}
274
275impl VerifiedOutcomeEvaluationV1 {
276 #[must_use]
277 pub const fn evaluation(&self) -> &OutcomeEvaluationV1 {
278 &self.evaluation
279 }
280
281 #[must_use]
282 pub fn output_digest(&self) -> &str {
283 &self.output_digest
284 }
285
286 #[must_use]
287 pub fn predicate_id(&self) -> &str {
288 &self.predicate_id
289 }
290
291 #[must_use]
292 pub fn predicate_digest(&self) -> &str {
293 &self.predicate_digest
294 }
295}
296
297pub fn evaluate_outcome_predicate(
298 predicate: &VerifiedOutcomePredicateV1,
299 output: &[u8],
300) -> VerifiedOutcomeEvaluationV1 {
301 VerifiedOutcomeEvaluationV1 {
302 evaluation: evaluate(predicate, output),
303 output_digest: sha256_hex(output),
304 predicate_id: predicate.body().predicate_id().to_owned(),
305 predicate_digest: predicate.envelope_digest().to_owned(),
306 }
307}
308
309fn evaluate(predicate: &VerifiedOutcomePredicateV1, output: &[u8]) -> OutcomeEvaluationV1 {
310 let Ok(output_text) = std::str::from_utf8(output) else {
311 return OutcomeEvaluationV1::Unevaluable {
312 reason: OutcomeEvaluationReasonV1::InvalidOutputJson,
313 };
314 };
315 if canonical_json_bytes_from_str(output_text).is_err() {
316 return OutcomeEvaluationV1::Unevaluable {
317 reason: OutcomeEvaluationReasonV1::InvalidOutputJson,
318 };
319 }
320 let document: Value = match serde_json::from_slice(output) {
321 Ok(document) => document,
322 Err(_) => {
323 return OutcomeEvaluationV1::Unevaluable {
324 reason: OutcomeEvaluationReasonV1::InvalidOutputJson,
325 };
326 }
327 };
328 for (index, assertion) in predicate.body().assertions.iter().enumerate() {
329 let Some(target) = select_pointer(&document, &assertion.pointer) else {
330 return OutcomeEvaluationV1::Failed {
331 assertion_index: u32::try_from(index).unwrap_or(u32::MAX),
332 reason: OutcomeEvaluationReasonV1::MissingTarget,
333 };
334 };
335 let passed = match &assertion.comparator {
336 OutcomeComparatorV1::Exists => true,
337 OutcomeComparatorV1::Eq { value } => canonical_equal(target, value),
338 OutcomeComparatorV1::Ne { value } => !canonical_equal(target, value),
339 OutcomeComparatorV1::Lt { value }
340 | OutcomeComparatorV1::Lte { value }
341 | OutcomeComparatorV1::Gt { value }
342 | OutcomeComparatorV1::Gte { value } => {
343 let Some(left) = integer(target) else {
344 return OutcomeEvaluationV1::Unevaluable {
345 reason: OutcomeEvaluationReasonV1::TargetNotInteger,
346 };
347 };
348 let Some(right) = integer(value) else {
349 return OutcomeEvaluationV1::Unevaluable {
350 reason: OutcomeEvaluationReasonV1::TargetNotInteger,
351 };
352 };
353 match &assertion.comparator {
354 OutcomeComparatorV1::Lt { .. } => left < right,
355 OutcomeComparatorV1::Lte { .. } => left <= right,
356 OutcomeComparatorV1::Gt { .. } => left > right,
357 OutcomeComparatorV1::Gte { .. } => left >= right,
358 _ => false,
359 }
360 }
361 };
362 if !passed {
363 return OutcomeEvaluationV1::Failed {
364 assertion_index: u32::try_from(index).unwrap_or(u32::MAX),
365 reason: OutcomeEvaluationReasonV1::AssertionMismatch,
366 };
367 }
368 }
369 OutcomeEvaluationV1::Passed
370}
371
372fn validate_pointer(pointer: &str) -> Result<(), OutcomeError> {
373 if pointer.is_empty() {
374 return Ok(());
375 }
376 if pointer.chars().count() > MAX_POINTER_CHARS
377 || !pointer.starts_with('/')
378 || pointer.chars().any(char::is_control)
379 {
380 return Err(OutcomeError::InvalidField("pointer"));
381 }
382 let bytes = pointer.as_bytes();
383 let mut index = 0;
384 while index < bytes.len() {
385 if bytes[index] == b'~' {
386 if index + 1 >= bytes.len() || !matches!(bytes[index + 1], b'0' | b'1') {
387 return Err(OutcomeError::InvalidField("pointer_escape"));
388 }
389 index += 2;
390 } else {
391 index += 1;
392 }
393 }
394 Ok(())
395}
396
397fn select_pointer<'a>(document: &'a Value, pointer: &str) -> Option<&'a Value> {
398 if pointer.is_empty() {
399 return Some(document);
400 }
401 let mut current = document;
402 for token in pointer.split('/').skip(1) {
403 let token = token.replace("~1", "/").replace("~0", "~");
404 current = match current {
405 Value::Object(object) => object.get(&token)?,
406 Value::Array(array) => {
407 let bytes = token.as_bytes();
408 let valid_index = token == "0"
409 || bytes.first().is_some_and(|first| {
410 first.is_ascii_digit()
411 && *first != b'0'
412 && bytes[1..].iter().all(u8::is_ascii_digit)
413 });
414 if !valid_index {
415 return None;
416 }
417 array.get(token.parse::<usize>().ok()?)?
418 }
419 _ => return None,
420 };
421 }
422 Some(current)
423}
424
425fn integer(value: &Value) -> Option<i128> {
426 value
427 .as_i64()
428 .map(i128::from)
429 .or_else(|| value.as_u64().map(i128::from))
430}
431
432fn canonical_equal(left: &Value, right: &Value) -> bool {
433 match (
434 canonical_outcome_bytes(left),
435 canonical_outcome_bytes(right),
436 ) {
437 (Ok(left), Ok(right)) => left == right,
438 _ => false,
439 }
440}