use std::borrow::Cow;
use std::collections::BTreeMap;
use crate::crypto;
use crate::descriptor::Descriptor;
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;
#[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>;
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MapResolver {
descriptors: BTreeMap<String, Vec<u8>>,
receipts: BTreeMap<String, Vec<u8>>,
values: BTreeMap<String, Scalar>,
}
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);
}
}
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()
}
}
#[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::EvalUnavailable(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,
EvalUnavailable(Error),
}
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 = Descriptor::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;
for h in r.inputs() {
let p = member("$.inputs", h);
let Some(bytes) = res.receipt(h) else {
return err(ErrorCode::InputUnavailable, &p, "no receipt offered for this input");
};
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() != h {
return err(ErrorCode::InputUnavailable, &p, "the receipt offered has another hash");
}
if !d.inputs.contains(&input.cell().function_name()) {
return err(
ErrorCode::InputUndeclared,
&p,
format!("{} is not a declared input", input.cell().function_name()),
);
}
if input.asof() > r.asof() {
return err(ErrorCode::TimeOrder, &p, "an input's asof is after the receipt's asof");
}
if latest_tx.as_ref().is_none_or(|t| input.tx() > t.as_str()) {
latest_tx = Some(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(value) = res.evaluate(r.cell(), r.asof()) else {
return Ok(Recomputed::EvalUnavailable(Error::new(
ErrorCode::EvalUnavailable,
"$.value",
"the resolver could not evaluate this function",
)));
};
if &value != r.value() {
return err(ErrorCode::ValueMismatch, "$.value", "recomputation gives a different type or value");
}
Ok(Recomputed::Yes)
}
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(())
}