1use std::borrow::Cow;
4use std::collections::BTreeMap;
5
6use crate::crypto;
7use crate::descriptor::FunctionDescriptor;
8use crate::error::{err, member, Error, ErrorCode, Result};
9use crate::json::{self, Value};
10use crate::keys::Keys;
11use crate::lexical;
12use crate::model::{Cell, Scalar};
13use crate::receipt::Receipt;
14use crate::slice::{nest, Manifest};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub enum Level {
19 Signed,
21 Recomputed,
23}
24
25impl Level {
26 pub const fn as_str(self) -> &'static str {
28 match self {
29 Level::Signed => "signed",
30 Level::Recomputed => "recomputed",
31 }
32 }
33}
34
35pub trait Resolver {
59 fn descriptor(&self, fn_hash: &str) -> Option<Cow<'_, [u8]>>;
65
66 fn receipt(&self, hash: &str) -> Option<Cow<'_, [u8]>>;
69
70 fn evaluate(&self, cell: &Cell, asof: &str) -> Option<Scalar>;
75
76 fn manifest(&self, entry: &str) -> Option<Cow<'_, [u8]>> {
81 let _ = entry;
82 None
83 }
84
85 fn evaluation(&self, cell: &Cell, asof: &str) -> Option<Evaluation> {
94 self.evaluate(cell, asof).map(Evaluation::new)
95 }
96}
97
98#[derive(Clone, Debug, PartialEq, Eq, Hash)]
103#[non_exhaustive]
104pub struct Evaluation {
105 pub value: Scalar,
107 pub lineage: Option<Vec<String>>,
109}
110
111impl Evaluation {
112 pub fn new(value: Scalar) -> Evaluation {
114 Evaluation { value, lineage: None }
115 }
116
117 pub fn with_lineage(value: Scalar, lineage: Vec<String>) -> Evaluation {
119 Evaluation { value, lineage: Some(lineage) }
120 }
121}
122
123#[derive(Clone, Debug, Default, PartialEq, Eq)]
134pub struct MapResolver {
135 descriptors: BTreeMap<String, Vec<u8>>,
136 receipts: BTreeMap<String, Vec<u8>>,
137 values: BTreeMap<String, Scalar>,
138 manifests: BTreeMap<String, Vec<u8>>,
139 lineages: BTreeMap<String, Vec<String>>,
140}
141
142impl MapResolver {
143 pub fn new() -> MapResolver {
145 MapResolver::default()
146 }
147
148 pub fn insert_descriptor(&mut self, fn_hash: impl Into<String>, json: impl Into<Vec<u8>>) {
150 self.descriptors.insert(fn_hash.into(), json.into());
151 }
152
153 pub fn insert_receipt(&mut self, hash: impl Into<String>, json: impl Into<Vec<u8>>) {
155 self.receipts.insert(hash.into(), json.into());
156 }
157
158 pub fn insert_value(&mut self, cell_id: impl Into<String>, value: Scalar) {
160 self.values.insert(cell_id.into(), value);
161 }
162
163 pub fn insert_manifest(&mut self, entry: impl Into<String>, json: impl Into<Vec<u8>>) {
165 self.manifests.insert(entry.into(), json.into());
166 }
167
168 pub fn insert_lineage<I, S>(&mut self, cell_id: impl Into<String>, lineage: I)
171 where
172 I: IntoIterator<Item = S>,
173 S: Into<String>,
174 {
175 self.lineages.insert(cell_id.into(), lineage.into_iter().map(Into::into).collect());
176 }
177}
178
179impl Resolver for MapResolver {
180 fn descriptor(&self, fn_hash: &str) -> Option<Cow<'_, [u8]>> {
181 self.descriptors.get(fn_hash).map(|b| Cow::Borrowed(b.as_slice()))
182 }
183
184 fn receipt(&self, hash: &str) -> Option<Cow<'_, [u8]>> {
185 self.receipts.get(hash).map(|b| Cow::Borrowed(b.as_slice()))
186 }
187
188 fn evaluate(&self, cell: &Cell, _asof: &str) -> Option<Scalar> {
189 self.values.get(&cell.id()).cloned()
190 }
191
192 fn manifest(&self, entry: &str) -> Option<Cow<'_, [u8]>> {
193 self.manifests.get(entry).map(|b| Cow::Borrowed(b.as_slice()))
194 }
195
196 fn evaluation(&self, cell: &Cell, _asof: &str) -> Option<Evaluation> {
197 let id = cell.id();
198 let value = self.values.get(&id)?.clone();
199 Some(match self.lineages.get(&id) {
200 Some(l) => Evaluation::with_lineage(value, l.clone()),
201 None => Evaluation::new(value),
202 })
203 }
204}
205
206#[derive(Clone, Debug, PartialEq, Eq)]
223pub struct VerifyResult {
224 pub ok: bool,
226 pub code: Option<ErrorCode>,
229 pub level: Option<Level>,
231 pub hash: Option<String>,
233 pub computed_by: Option<String>,
235 pub blob_checked: bool,
237 pub detail: String,
240 pub path: String,
242}
243
244impl VerifyResult {
245 pub fn error(&self) -> Option<Error> {
248 self.code.map(|c| Error::new(c, self.path.clone(), self.detail.clone()))
249 }
250}
251
252pub fn verify(receipt: &[u8], keys: &Keys, resolver: Option<&dyn Resolver>, blob: Option<&[u8]>) -> VerifyResult {
262 let mut out = VerifyResult {
263 ok: false,
264 code: None,
265 level: None,
266 hash: None,
267 computed_by: None,
268 blob_checked: false,
269 detail: String::new(),
270 path: String::new(),
271 };
272 let r = match signature_valid(receipt, keys) {
273 Ok(r) => r,
274 Err((e, r)) => {
275 if let Some(r) = r {
276 out.hash = Some(r.hash().to_string());
277 out.computed_by = Some(r.computed_by().to_string());
278 }
279 return fail(out, e);
280 }
281 };
282 out.hash = Some(r.hash().to_string());
283 out.computed_by = Some(r.computed_by().to_string());
284 let mut level = Level::Signed;
285 if let Some(res) = resolver {
286 match recompute_steps(&r, keys, res) {
287 Ok(Recomputed::Yes) => level = Level::Recomputed,
288 Ok(Recomputed::Signed(e)) => out = fail(out, e),
289 Err(e) => return fail(out, e),
290 }
291 }
292 if let Some(content) = blob {
293 if let Err(e) = check_blob(&r, content) {
294 return fail(out, e);
295 }
296 out.blob_checked = true;
297 }
298 out.ok = true;
299 out.level = Some(level);
300 out
301}
302
303fn fail(mut out: VerifyResult, e: Error) -> VerifyResult {
304 out.code = Some(e.code());
305 out.detail = e.detail().to_string();
306 out.path = e.path().to_string();
307 out
308}
309
310type StepError = (Error, Option<Box<Receipt>>);
312
313fn signature_valid(bytes: &[u8], keys: &Keys) -> core::result::Result<Receipt, StepError> {
315 let v = json::parse(bytes).map_err(|e| (e, None))?;
316 signature_valid_value(&v, keys)
317}
318
319pub(crate) fn signature_valid_value(v: &Value, keys: &Keys) -> core::result::Result<Receipt, StepError> {
321 let r = Receipt::from_value(v).map_err(|e| (e, None))?;
322 match key_and_signature(&r, keys) {
323 Ok(()) => Ok(r),
324 Err(e) => Err((e, Some(Box::new(r)))),
325 }
326}
327
328fn key_and_signature(r: &Receipt, keys: &Keys) -> Result<()> {
329 let doc = keys.get(r.signer_host()).map_err(|e| Error::new(e.code(), "$.computed_by", e.detail()))?;
330 let Some(key) = doc.key(r.key_id()) else {
331 return err(
332 ErrorCode::KeyUnknown,
333 "$.computed_by",
334 format!("{} publishes no key {}", r.signer_host(), r.key_id()),
335 );
336 };
337 if !key.usable_at(r.issued()) {
338 return err(ErrorCode::KeyNotValid, "$.issued", "issued is outside the key's validity window");
339 }
340 let Some(sig) = lexical::decode_hex_lower::<64>(r.sig()) else {
341 return err(ErrorCode::Signature, "$.sig", "signature is not 64 bytes");
342 };
343 if !crypto::verify_ed25519(&key.public, r.payload(), &sig) {
344 return err(ErrorCode::Signature, "$.sig", "Ed25519 verification failed");
345 }
346 Ok(())
347}
348
349enum Recomputed {
351 Yes,
353 Signed(Error),
356}
357
358struct Inputs {
360 cells: BTreeMap<String, (String, String)>,
361}
362
363fn recompute_steps(r: &Receipt, keys: &Keys, res: &dyn Resolver) -> Result<Recomputed> {
364 let Some(desc_bytes) = res.descriptor(&r.cell().fn_hash) else {
366 return err(ErrorCode::FnUnavailable, "$.cell.fn_hash", "no descriptor for fn_hash");
367 };
368 let desc_v = json::parse(&desc_bytes)?;
369 if crypto::sha256_hex(&desc_v.to_jcs()) != r.cell().fn_hash {
370 return err(ErrorCode::FnHashMismatch, "$.cell.fn_hash", "the descriptor does not hash to fn_hash");
371 }
372 let d = FunctionDescriptor::from_value(&desc_v)?;
373 let decl = &d.signature.args;
374 for k in r.cell().args.keys() {
376 if !decl.contains_key(k) {
377 return err(ErrorCode::ArgUnknown, &member("$.cell.args", k), "the function declares no such argument");
378 }
379 }
380 for k in decl.keys() {
381 if !r.cell().args.contains_key(k) {
382 return err(ErrorCode::ArgMissing, &member("$.cell.args", k), "a declared argument is not bound");
383 }
384 }
385 for (k, a) in &r.cell().args {
386 let declared = decl.get(k).map(|x| x.ty);
387 if a.scalar().map(Scalar::ty) != declared {
388 return err(ErrorCode::ArgType, &member("$.cell.args", k), "type differs from the declaration");
389 }
390 }
391 let declared_valid: Vec<&String> = decl.iter().filter(|(_, x)| x.valid).map(|(k, _)| k).collect();
393 let given_valid: Vec<&String> = r.valid().keys().collect();
394 if declared_valid != given_valid {
395 return err(
396 ErrorCode::ValidMismatch,
397 "$.valid",
398 "valid does not hold exactly the declared valid-time arguments",
399 );
400 }
401 if r.ty() != d.signature.returns.ty {
403 return err(ErrorCode::ReturnType, "$.type", "type differs from the declared return type");
404 }
405 let mut latest_tx: Option<String> = None;
407 let mut seen = Inputs { cells: BTreeMap::new() };
408 for e in r.inputs() {
409 let p = member("$.inputs", e);
410 if lexical::split_slice_entry(e).is_some() {
411 slice_entry(r, keys, res, &d, e, &p, &mut latest_tx)?;
412 continue;
413 }
414 let input = obtain(keys, res, e, &p, "input")?;
415 if !d.inputs.contains(&input.cell().function_name()) {
416 return err(
417 ErrorCode::InputUndeclared,
418 &p,
419 format!("{} is not a declared input", input.cell().function_name()),
420 );
421 }
422 input_asof(r, &input, &p)?;
423 later_tx(&mut latest_tx, input.tx());
424 seen.cells.insert(e.clone(), (input.cell().id(), input.tx().to_string()));
425 }
426 if let Some(t) = latest_tx {
427 if r.tx() != t {
428 return err(ErrorCode::TimeOrder, "$.tx", "tx must equal the latest input tx");
429 }
430 }
431 let Some(ev) = res.evaluation(r.cell(), r.asof()) else {
433 return Ok(Recomputed::Signed(Error::new(
434 ErrorCode::EvalUnavailable,
435 "$.value",
436 "the resolver could not evaluate this function",
437 )));
438 };
439 if &ev.value == r.value() {
440 return Ok(Recomputed::Yes);
441 }
442 if let Some(lineage) = &ev.lineage {
443 if let Some(detail) = superseded(r, keys, res, &seen, lineage) {
444 return Ok(Recomputed::Signed(Error::new(ErrorCode::InputSuperseded, "$.inputs", detail)));
445 }
446 }
447 err(ErrorCode::ValueMismatch, "$.value", "recomputation gives a different type or value")
448}
449
450fn later_tx(latest: &mut Option<String>, tx: &str) {
451 if latest.as_ref().is_none_or(|t| tx > t.as_str()) {
452 *latest = Some(tx.to_string());
453 }
454}
455
456fn obtain(keys: &Keys, res: &dyn Resolver, hash: &str, p: &str, what: &str) -> Result<Receipt> {
460 let Some(bytes) = res.receipt(hash) else {
461 return err(ErrorCode::InputUnavailable, p, format!("no receipt offered for this {what}"));
462 };
463 let input = match signature_valid(&bytes, keys) {
464 Ok(i) => i,
465 Err((e, _)) => return err(ErrorCode::InputInvalid, p, format!("{}: {}", e.code(), e.detail())),
466 };
467 if input.hash() != hash {
468 return err(ErrorCode::InputUnavailable, p, format!("the receipt offered for this {what} has another hash"));
469 }
470 Ok(input)
471}
472
473fn input_asof(r: &Receipt, input: &Receipt, p: &str) -> Result<()> {
476 if input.asof() > r.asof() {
477 return err(ErrorCode::TimeOrder, p, "an input's asof is after the receipt's asof");
478 }
479 Ok(())
480}
481
482fn slice_entry(
487 r: &Receipt,
488 keys: &Keys,
489 res: &dyn Resolver,
490 d: &FunctionDescriptor,
491 entry: &str,
492 p: &str,
493 latest_tx: &mut Option<String>,
494) -> Result<()> {
495 let Some((slice_id, members_hash)) = lexical::split_slice_entry(entry) else {
496 return err(ErrorCode::CanonHex, p, "not a slice entry");
497 };
498 let Some(bytes) = res.manifest(entry) else {
499 return err(ErrorCode::InputUnavailable, p, "no manifest offered for this slice entry");
500 };
501 let m = Manifest::from_json(&bytes).map_err(|e| nest(e, p))?;
502 if m.slice().id() != slice_id || m.members_hash() != members_hash {
503 return err(ErrorCode::InputUnavailable, p, "the manifest offered does not hash to the entry");
504 }
505 if !d.inputs.contains(&m.slice().function_name()) {
506 return err(
507 ErrorCode::InputUndeclared,
508 p,
509 format!("the slice's function {} is not a declared input", m.slice().function_name()),
510 );
511 }
512 let mut cells = std::collections::BTreeSet::new();
513 for h in m.members() {
514 let mp = format!("{p}.members.{h}");
515 let member = obtain(keys, res, h, &mp, "member")?;
516 if !m.slice().contains(&member) {
517 return err(ErrorCode::SliceMember, &mp, "the member does not belong to the slice");
518 }
519 if !m.slice().is_version_slice() && !cells.insert(member.cell().id()) {
520 return err(ErrorCode::SliceMember, &mp, "two members of the slice are one cell");
521 }
522 input_asof(r, &member, &mp)?;
523 later_tx(latest_tx, member.tx());
524 }
525 Ok(())
526}
527
528fn superseded(r: &Receipt, keys: &Keys, res: &dyn Resolver, seen: &Inputs, lineage: &[String]) -> Option<String> {
535 let dropped: Vec<&String> = r.inputs().iter().filter(|e| !lineage.contains(e)).collect();
536 if dropped.is_empty() {
537 return None;
538 }
539 let dropped_slices: Vec<&str> =
540 dropped.iter().filter_map(|e| lexical::split_slice_entry(e)).map(|(id, _)| id).collect();
541 let dropped_cells: Vec<&(String, String)> = dropped.iter().filter_map(|e| seen.cells.get(*e)).collect();
542 for e in lineage.iter().filter(|e| !r.inputs().contains(e)) {
543 if e.starts_with("slice:") {
546 if let Some(id) = dropped_slices.iter().find(|id| e.starts_with(&format!("slice:{id}:"))) {
547 return Some(format!("the evaluation read slice {id} with other members ({e})"));
548 }
549 continue;
550 }
551 if dropped_cells.is_empty() || lexical::check_hex(e, 64, "").is_err() {
552 continue;
553 }
554 let Ok(later) = obtain(keys, res, e, "", "receipt") else { continue };
555 let id = later.cell().id();
556 if dropped_cells.iter().any(|(cell, tx)| *cell == id && later.tx() > tx.as_str()) {
557 return Some(format!("the evaluation read a later version of an input's cell, receipt {e}"));
558 }
559 }
560 None
561}
562
563fn check_blob(r: &Receipt, content: &[u8]) -> Result<()> {
564 let Scalar::Blob(b) = &r.value() else {
565 return err(ErrorCode::Schema, "$.value", "content supplied for a value that is not a blob");
566 };
567 let Ok((hash, len, _)) = lexical::parse_blob(b) else {
568 return err(ErrorCode::CanonBlob, "$.value", "not a blob");
569 };
570 if content.len() as u64 != len {
571 return err(
572 ErrorCode::BlobMismatch,
573 "$.value",
574 format!("content has {} bytes, the blob states {len}", content.len()),
575 );
576 }
577 if crypto::sha256(content) != hash {
578 return err(ErrorCode::BlobMismatch, "$.value", "content does not hash to the blob's sha256");
579 }
580 Ok(())
581}