Skip to main content

datumesh_core/
verify.rs

1//! The verification procedure (10.1).
2
3use std::collections::BTreeMap;
4
5use crate::crypto;
6use crate::descriptor::Descriptor;
7use crate::error::{err, member, Error, ErrorCode, Result};
8use crate::json::{self, Value};
9use crate::keys::Keys;
10use crate::lexical;
11use crate::model::Scalar;
12use crate::receipt::Receipt;
13
14/// How far a receipt verified.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum Level {
17    /// Steps 1-6 passed: the holder of `computed_by` stated this value.
18    Signed,
19    /// Steps 7.1-7.6 passed as well.
20    Recomputed,
21}
22
23impl Level {
24    /// `"signed"` or `"recomputed"`.
25    pub const fn as_str(self) -> &'static str {
26        match self {
27            Level::Signed => "signed",
28            Level::Recomputed => "recomputed",
29        }
30    }
31}
32
33/// What step 7 needs, supplied by the caller (this crate does not evaluate functions or
34/// fetch anything).
35#[derive(Clone, Debug)]
36pub struct Recompute {
37    /// The descriptor JSON text for `cell.fn_hash`; `None` is `E_FN_UNAVAILABLE`.
38    pub descriptor: Option<Vec<u8>>,
39    /// Input receipts as JSON text, keyed by the receipt hash they were offered for.
40    pub inputs: BTreeMap<String, Vec<u8>>,
41    /// The result of evaluating the function (for `stored`: the value looked up as of `asof`).
42    pub value: Scalar,
43}
44
45/// The outcome of [`verify`].
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct VerifyResult {
48    /// Whether the receipt verified.
49    pub ok: bool,
50    /// The failure's code; `None` when `ok`.
51    pub code: Option<ErrorCode>,
52    /// The level reached; `None` unless `ok`.
53    pub level: Option<Level>,
54    /// The receipt hash, once the receipt was canonical enough to compute it.
55    pub hash: Option<String>,
56    /// `computed_by`, once known.
57    pub key: Option<String>,
58    /// Whether step 7.7 ran and passed.
59    pub blob_checked: bool,
60    /// A human explanation of the failure; empty when `ok`.
61    pub detail: String,
62    /// Where the failure was found; empty when `ok`.
63    pub path: String,
64}
65
66impl VerifyResult {
67    /// The failure as an [`Error`], if there is one.
68    pub fn error(&self) -> Option<Error> {
69        self.code.map(|c| Error::new(c, self.path.clone(), self.detail.clone()))
70    }
71}
72
73/// Verifies a receipt (10.1). Never touches the network and never panics.
74///
75/// * `receipt`: the receipt's JSON text.
76/// * `keys`: the keys resolved for signer hosts (absent hosts are `E_KEY_UNAVAILABLE`).
77/// * `recompute`: when given, step 7.1-7.6 run, and success is level `recomputed`.
78/// * `blob`: content for step 7.7 (the value must be a blob); success sets `blob_checked`.
79pub fn verify(receipt: &[u8], keys: &Keys, recompute: Option<&Recompute>, blob: Option<&[u8]>) -> VerifyResult {
80    let mut out = VerifyResult {
81        ok: false,
82        code: None,
83        level: None,
84        hash: None,
85        key: None,
86        blob_checked: false,
87        detail: String::new(),
88        path: String::new(),
89    };
90    let r = match signature_valid(receipt, keys) {
91        Ok(r) => r,
92        Err((e, r)) => {
93            if let Some(r) = r {
94                out.hash = Some(r.hash());
95                out.key = Some(r.computed_by.clone());
96            }
97            return fail(out, e);
98        }
99    };
100    out.hash = Some(r.hash());
101    out.key = Some(r.computed_by.clone());
102    let mut level = Level::Signed;
103    if let Some(rc) = recompute {
104        if let Err(e) = recompute_steps(&r, keys, rc) {
105            return fail(out, e);
106        }
107        level = Level::Recomputed;
108    }
109    if let Some(content) = blob {
110        if let Err(e) = check_blob(&r, content) {
111            return fail(out, e);
112        }
113        out.blob_checked = true;
114    }
115    out.ok = true;
116    out.level = Some(level);
117    out
118}
119
120fn fail(mut out: VerifyResult, e: Error) -> VerifyResult {
121    out.code = Some(e.code());
122    out.detail = e.detail().to_string();
123    out.path = e.path().to_string();
124    out
125}
126
127/// A failure in steps 1-6, with the receipt when steps 1-4 passed.
128type StepError = (Error, Option<Box<Receipt>>);
129
130/// Steps 1-6 over JSON text.
131fn signature_valid(bytes: &[u8], keys: &Keys) -> core::result::Result<Receipt, StepError> {
132    let v = json::parse(bytes).map_err(|e| (e, None))?;
133    signature_valid_value(&v, keys)
134}
135
136/// Steps 2-6 over a parsed value (also used for receipts inside envelopes).
137pub(crate) fn signature_valid_value(v: &Value, keys: &Keys) -> core::result::Result<Receipt, StepError> {
138    let r = Receipt::from_value(v).map_err(|e| (e, None))?;
139    match key_and_signature(&r, keys) {
140        Ok(()) => Ok(r),
141        Err(e) => Err((e, Some(Box::new(r)))),
142    }
143}
144
145fn key_and_signature(r: &Receipt, keys: &Keys) -> Result<()> {
146    let doc = keys.get(r.signer_host()).map_err(|e| Error::new(e.code(), "$.computed_by", e.detail()))?;
147    let Some(key) = doc.key(r.key_id()) else {
148        return err(
149            ErrorCode::KeyUnknown,
150            "$.computed_by",
151            format!("{} publishes no key {}", r.signer_host(), r.key_id()),
152        );
153    };
154    if !key.usable_at(&r.issued) {
155        return err(ErrorCode::KeyNotValid, "$.issued", "issued is outside the key's validity window");
156    }
157    let Some(sig) = lexical::decode_hex_lower::<64>(&r.sig) else {
158        return err(ErrorCode::Signature, "$.sig", "signature is not 64 bytes");
159    };
160    if !crypto::verify_ed25519(&key.public, r.payload(), &sig) {
161        return err(ErrorCode::Signature, "$.sig", "Ed25519 verification failed");
162    }
163    Ok(())
164}
165
166fn recompute_steps(r: &Receipt, keys: &Keys, rc: &Recompute) -> Result<()> {
167    // 7.1
168    let Some(desc_bytes) = &rc.descriptor else {
169        return err(ErrorCode::FnUnavailable, "$.cell.fn_hash", "no descriptor for fn_hash");
170    };
171    let desc_v = json::parse(desc_bytes)?;
172    if crypto::sha256_hex(&desc_v.to_jcs()) != r.cell.fn_hash {
173        return err(ErrorCode::FnHashMismatch, "$.cell.fn_hash", "the descriptor does not hash to fn_hash");
174    }
175    let d = Descriptor::from_value(&desc_v)?;
176    let decl = &d.signature.args;
177    // 7.2: unknown, then missing, then types, each in JCS order of names.
178    for k in r.cell.args.keys() {
179        if !decl.contains_key(k) {
180            return err(ErrorCode::ArgUnknown, &member("$.cell.args", k), "the function declares no such argument");
181        }
182    }
183    for k in decl.keys() {
184        if !r.cell.args.contains_key(k) {
185            return err(ErrorCode::ArgMissing, &member("$.cell.args", k), "a declared argument is not bound");
186        }
187    }
188    for (k, a) in &r.cell.args {
189        let declared = decl.get(k).map(|x| x.ty);
190        if a.scalar().map(Scalar::ty) != declared {
191            return err(ErrorCode::ArgType, &member("$.cell.args", k), "type differs from the declaration");
192        }
193    }
194    // 7.3
195    let declared_valid: Vec<&String> = decl.iter().filter(|(_, x)| x.valid).map(|(k, _)| k).collect();
196    let given_valid: Vec<&String> = r.valid.keys().collect();
197    if declared_valid != given_valid {
198        return err(
199            ErrorCode::ValidMismatch,
200            "$.valid",
201            "valid does not hold exactly the declared valid-time arguments",
202        );
203    }
204    // 7.4
205    if r.ty() != d.signature.returns.ty {
206        return err(ErrorCode::ReturnType, "$.type", "type differs from the declared return type");
207    }
208    // 7.5
209    let mut latest_tx: Option<String> = None;
210    for h in &r.inputs {
211        let p = member("$.inputs", h);
212        let Some(bytes) = rc.inputs.get(h) else {
213            return err(ErrorCode::InputUnavailable, &p, "no receipt offered for this input");
214        };
215        let input = match signature_valid(bytes, keys) {
216            Ok(i) => i,
217            Err((e, _)) => return err(ErrorCode::InputInvalid, &p, format!("{}: {}", e.code(), e.detail())),
218        };
219        if &input.hash() != h {
220            return err(ErrorCode::InputUnavailable, &p, "the receipt offered has another hash");
221        }
222        if !d.inputs.contains(&input.cell.function_name()) {
223            return err(
224                ErrorCode::InputUndeclared,
225                &p,
226                format!("{} is not a declared input", input.cell.function_name()),
227            );
228        }
229        if input.asof != r.asof {
230            return err(ErrorCode::TimeOrder, &p, "an input has another asof");
231        }
232        if latest_tx.as_ref().is_none_or(|t| input.tx > *t) {
233            latest_tx = Some(input.tx.clone());
234        }
235    }
236    if let Some(t) = latest_tx {
237        if r.tx != t {
238            return err(ErrorCode::TimeOrder, "$.tx", "tx must equal the latest input tx");
239        }
240    }
241    // 7.6
242    if rc.value != r.value {
243        return err(ErrorCode::ValueMismatch, "$.value", "recomputation gives a different type or value");
244    }
245    Ok(())
246}
247
248fn check_blob(r: &Receipt, content: &[u8]) -> Result<()> {
249    let Scalar::Blob(b) = &r.value else {
250        return err(ErrorCode::Schema, "$.value", "content supplied for a value that is not a blob");
251    };
252    let Ok((hash, len, _)) = lexical::parse_blob(b) else {
253        return err(ErrorCode::CanonBlob, "$.value", "not a blob");
254    };
255    if content.len() as u64 != len {
256        return err(
257            ErrorCode::BlobMismatch,
258            "$.value",
259            format!("content has {} bytes, the blob states {len}", content.len()),
260        );
261    }
262    if crypto::sha256(content) != hash {
263        return err(ErrorCode::BlobMismatch, "$.value", "content does not hash to the blob's sha256");
264    }
265    Ok(())
266}