1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum Level {
17 Signed,
19 Recomputed,
21}
22
23impl Level {
24 pub const fn as_str(self) -> &'static str {
26 match self {
27 Level::Signed => "signed",
28 Level::Recomputed => "recomputed",
29 }
30 }
31}
32
33#[derive(Clone, Debug)]
36pub struct Recompute {
37 pub descriptor: Option<Vec<u8>>,
39 pub inputs: BTreeMap<String, Vec<u8>>,
41 pub value: Scalar,
43}
44
45#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct VerifyResult {
48 pub ok: bool,
50 pub code: Option<ErrorCode>,
52 pub level: Option<Level>,
54 pub hash: Option<String>,
56 pub key: Option<String>,
58 pub blob_checked: bool,
60 pub detail: String,
62 pub path: String,
64}
65
66impl VerifyResult {
67 pub fn error(&self) -> Option<Error> {
69 self.code.map(|c| Error::new(c, self.path.clone(), self.detail.clone()))
70 }
71}
72
73pub 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
127type StepError = (Error, Option<Box<Receipt>>);
129
130fn 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
136pub(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 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 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 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 if r.ty() != d.signature.returns.ty {
206 return err(ErrorCode::ReturnType, "$.type", "type differs from the declared return type");
207 }
208 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 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}