use std::borrow::Cow;
use std::collections::BTreeMap;
use crate::crypto;
use crate::descriptor::FunctionDescriptor;
use crate::error::{err, member, Error, ErrorCode, Result};
use crate::json::{self, Value};
use crate::keys::Keys;
use crate::lexical;
use crate::model::{Cell, Scalar};
use crate::receipt::Receipt;
use crate::slice::{nest, Manifest};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Level {
Signed,
Recomputed,
}
impl Level {
pub const fn as_str(self) -> &'static str {
match self {
Level::Signed => "signed",
Level::Recomputed => "recomputed",
}
}
}
pub trait Resolver {
fn descriptor(&self, fn_hash: &str) -> Option<Cow<'_, [u8]>>;
fn receipt(&self, hash: &str) -> Option<Cow<'_, [u8]>>;
fn evaluate(&self, cell: &Cell, asof: &str) -> Option<Scalar>;
fn manifest(&self, entry: &str) -> Option<Cow<'_, [u8]>> {
let _ = entry;
None
}
fn evaluation(&self, cell: &Cell, asof: &str) -> Option<Evaluation> {
self.evaluate(cell, asof).map(Evaluation::new)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Evaluation {
pub value: Scalar,
pub lineage: Option<Vec<String>>,
}
impl Evaluation {
pub fn new(value: Scalar) -> Evaluation {
Evaluation { value, lineage: None }
}
pub fn with_lineage(value: Scalar, lineage: Vec<String>) -> Evaluation {
Evaluation { value, lineage: Some(lineage) }
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MapResolver {
descriptors: BTreeMap<String, Vec<u8>>,
receipts: BTreeMap<String, Vec<u8>>,
values: BTreeMap<String, Scalar>,
manifests: BTreeMap<String, Vec<u8>>,
lineages: BTreeMap<String, Vec<String>>,
}
impl MapResolver {
pub fn new() -> MapResolver {
MapResolver::default()
}
pub fn insert_descriptor(&mut self, fn_hash: impl Into<String>, json: impl Into<Vec<u8>>) {
self.descriptors.insert(fn_hash.into(), json.into());
}
pub fn insert_receipt(&mut self, hash: impl Into<String>, json: impl Into<Vec<u8>>) {
self.receipts.insert(hash.into(), json.into());
}
pub fn insert_value(&mut self, cell_id: impl Into<String>, value: Scalar) {
self.values.insert(cell_id.into(), value);
}
pub fn insert_manifest(&mut self, entry: impl Into<String>, json: impl Into<Vec<u8>>) {
self.manifests.insert(entry.into(), json.into());
}
pub fn insert_lineage<I, S>(&mut self, cell_id: impl Into<String>, lineage: I)
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.lineages.insert(cell_id.into(), lineage.into_iter().map(Into::into).collect());
}
}
impl Resolver for MapResolver {
fn descriptor(&self, fn_hash: &str) -> Option<Cow<'_, [u8]>> {
self.descriptors.get(fn_hash).map(|b| Cow::Borrowed(b.as_slice()))
}
fn receipt(&self, hash: &str) -> Option<Cow<'_, [u8]>> {
self.receipts.get(hash).map(|b| Cow::Borrowed(b.as_slice()))
}
fn evaluate(&self, cell: &Cell, _asof: &str) -> Option<Scalar> {
self.values.get(&cell.id()).cloned()
}
fn manifest(&self, entry: &str) -> Option<Cow<'_, [u8]>> {
self.manifests.get(entry).map(|b| Cow::Borrowed(b.as_slice()))
}
fn evaluation(&self, cell: &Cell, _asof: &str) -> Option<Evaluation> {
let id = cell.id();
let value = self.values.get(&id)?.clone();
Some(match self.lineages.get(&id) {
Some(l) => Evaluation::with_lineage(value, l.clone()),
None => Evaluation::new(value),
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VerifyResult {
pub ok: bool,
pub code: Option<ErrorCode>,
pub level: Option<Level>,
pub hash: Option<String>,
pub computed_by: Option<String>,
pub blob_checked: bool,
pub detail: String,
pub path: String,
}
impl VerifyResult {
pub fn error(&self) -> Option<Error> {
self.code.map(|c| Error::new(c, self.path.clone(), self.detail.clone()))
}
}
pub fn verify(receipt: &[u8], keys: &Keys, resolver: Option<&dyn Resolver>, blob: Option<&[u8]>) -> VerifyResult {
let mut out = VerifyResult {
ok: false,
code: None,
level: None,
hash: None,
computed_by: None,
blob_checked: false,
detail: String::new(),
path: String::new(),
};
let r = match signature_valid(receipt, keys) {
Ok(r) => r,
Err((e, r)) => {
if let Some(r) = r {
out.hash = Some(r.hash().to_string());
out.computed_by = Some(r.computed_by().to_string());
}
return fail(out, e);
}
};
out.hash = Some(r.hash().to_string());
out.computed_by = Some(r.computed_by().to_string());
let mut level = Level::Signed;
if let Some(res) = resolver {
match recompute_steps(&r, keys, res) {
Ok(Recomputed::Yes) => level = Level::Recomputed,
Ok(Recomputed::Signed(e)) => out = fail(out, e),
Err(e) => return fail(out, e),
}
}
if let Some(content) = blob {
if let Err(e) = check_blob(&r, content) {
return fail(out, e);
}
out.blob_checked = true;
}
out.ok = true;
out.level = Some(level);
out
}
fn fail(mut out: VerifyResult, e: Error) -> VerifyResult {
out.code = Some(e.code());
out.detail = e.detail().to_string();
out.path = e.path().to_string();
out
}
type StepError = (Error, Option<Box<Receipt>>);
fn signature_valid(bytes: &[u8], keys: &Keys) -> core::result::Result<Receipt, StepError> {
let v = json::parse(bytes).map_err(|e| (e, None))?;
signature_valid_value(&v, keys)
}
pub(crate) fn signature_valid_value(v: &Value, keys: &Keys) -> core::result::Result<Receipt, StepError> {
let r = Receipt::from_value(v).map_err(|e| (e, None))?;
match key_and_signature(&r, keys) {
Ok(()) => Ok(r),
Err(e) => Err((e, Some(Box::new(r)))),
}
}
fn key_and_signature(r: &Receipt, keys: &Keys) -> Result<()> {
let doc = keys.get(r.signer_host()).map_err(|e| Error::new(e.code(), "$.computed_by", e.detail()))?;
let Some(key) = doc.key(r.key_id()) else {
return err(
ErrorCode::KeyUnknown,
"$.computed_by",
format!("{} publishes no key {}", r.signer_host(), r.key_id()),
);
};
if !key.usable_at(r.issued()) {
return err(ErrorCode::KeyNotValid, "$.issued", "issued is outside the key's validity window");
}
let Some(sig) = lexical::decode_hex_lower::<64>(r.sig()) else {
return err(ErrorCode::Signature, "$.sig", "signature is not 64 bytes");
};
if !crypto::verify_ed25519(&key.public, r.payload(), &sig) {
return err(ErrorCode::Signature, "$.sig", "Ed25519 verification failed");
}
Ok(())
}
enum Recomputed {
Yes,
Signed(Error),
}
struct Inputs {
cells: BTreeMap<String, (String, String)>,
}
fn recompute_steps(r: &Receipt, keys: &Keys, res: &dyn Resolver) -> Result<Recomputed> {
let Some(desc_bytes) = res.descriptor(&r.cell().fn_hash) else {
return err(ErrorCode::FnUnavailable, "$.cell.fn_hash", "no descriptor for fn_hash");
};
let desc_v = json::parse(&desc_bytes)?;
if crypto::sha256_hex(&desc_v.to_jcs()) != r.cell().fn_hash {
return err(ErrorCode::FnHashMismatch, "$.cell.fn_hash", "the descriptor does not hash to fn_hash");
}
let d = FunctionDescriptor::from_value(&desc_v)?;
let decl = &d.signature.args;
for k in r.cell().args.keys() {
if !decl.contains_key(k) {
return err(ErrorCode::ArgUnknown, &member("$.cell.args", k), "the function declares no such argument");
}
}
for k in decl.keys() {
if !r.cell().args.contains_key(k) {
return err(ErrorCode::ArgMissing, &member("$.cell.args", k), "a declared argument is not bound");
}
}
for (k, a) in &r.cell().args {
let declared = decl.get(k).map(|x| x.ty);
if a.scalar().map(Scalar::ty) != declared {
return err(ErrorCode::ArgType, &member("$.cell.args", k), "type differs from the declaration");
}
}
let declared_valid: Vec<&String> = decl.iter().filter(|(_, x)| x.valid).map(|(k, _)| k).collect();
let given_valid: Vec<&String> = r.valid().keys().collect();
if declared_valid != given_valid {
return err(
ErrorCode::ValidMismatch,
"$.valid",
"valid does not hold exactly the declared valid-time arguments",
);
}
if r.ty() != d.signature.returns.ty {
return err(ErrorCode::ReturnType, "$.type", "type differs from the declared return type");
}
let mut latest_tx: Option<String> = None;
let mut seen = Inputs { cells: BTreeMap::new() };
for e in r.inputs() {
let p = member("$.inputs", e);
if lexical::split_slice_entry(e).is_some() {
slice_entry(r, keys, res, &d, e, &p, &mut latest_tx)?;
continue;
}
let input = obtain(keys, res, e, &p, "input")?;
if !d.inputs.contains(&input.cell().function_name()) {
return err(
ErrorCode::InputUndeclared,
&p,
format!("{} is not a declared input", input.cell().function_name()),
);
}
input_asof(r, &input, &p)?;
later_tx(&mut latest_tx, input.tx());
seen.cells.insert(e.clone(), (input.cell().id(), input.tx().to_string()));
}
if let Some(t) = latest_tx {
if r.tx() != t {
return err(ErrorCode::TimeOrder, "$.tx", "tx must equal the latest input tx");
}
}
let Some(ev) = res.evaluation(r.cell(), r.asof()) else {
return Ok(Recomputed::Signed(Error::new(
ErrorCode::EvalUnavailable,
"$.value",
"the resolver could not evaluate this function",
)));
};
if &ev.value == r.value() {
return Ok(Recomputed::Yes);
}
if let Some(lineage) = &ev.lineage {
if let Some(detail) = superseded(r, keys, res, &seen, lineage) {
return Ok(Recomputed::Signed(Error::new(ErrorCode::InputSuperseded, "$.inputs", detail)));
}
}
err(ErrorCode::ValueMismatch, "$.value", "recomputation gives a different type or value")
}
fn later_tx(latest: &mut Option<String>, tx: &str) {
if latest.as_ref().is_none_or(|t| tx > t.as_str()) {
*latest = Some(tx.to_string());
}
}
fn obtain(keys: &Keys, res: &dyn Resolver, hash: &str, p: &str, what: &str) -> Result<Receipt> {
let Some(bytes) = res.receipt(hash) else {
return err(ErrorCode::InputUnavailable, p, format!("no receipt offered for this {what}"));
};
let input = match signature_valid(&bytes, keys) {
Ok(i) => i,
Err((e, _)) => return err(ErrorCode::InputInvalid, p, format!("{}: {}", e.code(), e.detail())),
};
if input.hash() != hash {
return err(ErrorCode::InputUnavailable, p, format!("the receipt offered for this {what} has another hash"));
}
Ok(input)
}
fn input_asof(r: &Receipt, input: &Receipt, p: &str) -> Result<()> {
if input.asof() > r.asof() {
return err(ErrorCode::TimeOrder, p, "an input's asof is after the receipt's asof");
}
Ok(())
}
fn slice_entry(
r: &Receipt,
keys: &Keys,
res: &dyn Resolver,
d: &FunctionDescriptor,
entry: &str,
p: &str,
latest_tx: &mut Option<String>,
) -> Result<()> {
let Some((slice_id, members_hash)) = lexical::split_slice_entry(entry) else {
return err(ErrorCode::CanonHex, p, "not a slice entry");
};
let Some(bytes) = res.manifest(entry) else {
return err(ErrorCode::InputUnavailable, p, "no manifest offered for this slice entry");
};
let m = Manifest::from_json(&bytes).map_err(|e| nest(e, p))?;
if m.slice().id() != slice_id || m.members_hash() != members_hash {
return err(ErrorCode::InputUnavailable, p, "the manifest offered does not hash to the entry");
}
if !d.inputs.contains(&m.slice().function_name()) {
return err(
ErrorCode::InputUndeclared,
p,
format!("the slice's function {} is not a declared input", m.slice().function_name()),
);
}
let mut cells = std::collections::BTreeSet::new();
for h in m.members() {
let mp = format!("{p}.members.{h}");
let member = obtain(keys, res, h, &mp, "member")?;
if !m.slice().contains(&member) {
return err(ErrorCode::SliceMember, &mp, "the member does not belong to the slice");
}
if !m.slice().is_version_slice() && !cells.insert(member.cell().id()) {
return err(ErrorCode::SliceMember, &mp, "two members of the slice are one cell");
}
input_asof(r, &member, &mp)?;
later_tx(latest_tx, member.tx());
}
Ok(())
}
fn superseded(r: &Receipt, keys: &Keys, res: &dyn Resolver, seen: &Inputs, lineage: &[String]) -> Option<String> {
let dropped: Vec<&String> = r.inputs().iter().filter(|e| !lineage.contains(e)).collect();
if dropped.is_empty() {
return None;
}
let dropped_slices: Vec<&str> =
dropped.iter().filter_map(|e| lexical::split_slice_entry(e)).map(|(id, _)| id).collect();
let dropped_cells: Vec<&(String, String)> = dropped.iter().filter_map(|e| seen.cells.get(*e)).collect();
for e in lineage.iter().filter(|e| !r.inputs().contains(e)) {
if e.starts_with("slice:") {
if let Some(id) = dropped_slices.iter().find(|id| e.starts_with(&format!("slice:{id}:"))) {
return Some(format!("the evaluation read slice {id} with other members ({e})"));
}
continue;
}
if dropped_cells.is_empty() || lexical::check_hex(e, 64, "").is_err() {
continue;
}
let Ok(later) = obtain(keys, res, e, "", "receipt") else { continue };
let id = later.cell().id();
if dropped_cells.iter().any(|(cell, tx)| *cell == id && later.tx() > tx.as_str()) {
return Some(format!("the evaluation read a later version of an input's cell, receipt {e}"));
}
}
None
}
fn check_blob(r: &Receipt, content: &[u8]) -> Result<()> {
let Scalar::Blob(b) = &r.value() else {
return err(ErrorCode::Schema, "$.value", "content supplied for a value that is not a blob");
};
let Ok((hash, len, _)) = lexical::parse_blob(b) else {
return err(ErrorCode::CanonBlob, "$.value", "not a blob");
};
if content.len() as u64 != len {
return err(
ErrorCode::BlobMismatch,
"$.value",
format!("content has {} bytes, the blob states {len}", content.len()),
);
}
if crypto::sha256(content) != hash {
return err(ErrorCode::BlobMismatch, "$.value", "content does not hash to the blob's sha256");
}
Ok(())
}