Hash chain
A tamper-evident audit log: each entry's SHA-256 covers the one before, so a changed, removed or reordered entry is found.
Version 1.0.0 · 3 functions · 1 crate
Functions
What use hash_chain.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use hash_chain_append, hash_chain_verify from hash_chain (Quillo 0.6).
hash_chain_append(chain: list, data: any) → anyor an errorThe next entry for chain (the entries so far, oldest first; [] for a new log) holding data: { index, at, data, prev, hash }, at in milliseconds since 1970, prev the last entry's hash (64 zeros for the first). Store it as it is, after the others; only its own fields and the last entry are read, so a long log can pass only its last entry.
hash_chain_verify(chain: list) → anyChecks a whole chain, oldest first: { ok, length, broken_at, reason }. broken_at is the index of the first entry that was changed, removed, put in another place or added in the middle, null when all is well. A chain cut at its end still checks: keep the last hash elsewhere (another database, a daily email) to find that.
hash_chain_hash(entry: any) → strThe hash an entry should have, worked out again from its index, at, data and prev: to check one entry alone.
Get it
$ quillo install hash_chain # hash_chain.qwer into this folder$ quillo install hash_chain --example # and its example, hash_chain.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/hash_chain.qwer$ curl -O https://quillolang.org/plugins/files/hash_chain.qweThen, in your program, all of it or only what you call:
use hash_chain.qweruse hash_chain_append, hash_chain_verify from hash_chain // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
sha2 = "0.10"Example
// An audit log of admin actions in which no past entry can be changed unseen.//// quillo run hash_chain.qwe then open http://localhost:8080/demouse "hash_chain.qwer"fn log_action(log: list, who: str, action: str) = [...log, hash_chain_append(log, { who, action })?]route GET /demo { let log = log_action(log_action(log_action([], "ada", "refund order 1042"), "grace", "change price of B7"), "ada", "delete user 77") write_file("data/audit.json", json.stringify(log, true)) let stored = json.parse(read_file("data/audit.json")?)? // Someone edits the second entry in the file afterwards. let edited = [stored[0], { ...stored[1], data: { who: "grace", action: "change price of B8" } }, stored[2]] { entries: len(stored), last_hash: last(stored).hash, intact: hash_chain_verify(stored), edited: hash_chain_verify(edited), removed: hash_chain_verify([stored[0], stored[2]]).reason, }}$ quillo run hash_chain.qwe$ curl 'localhost:8080/demo'Source
//! A tamper-evident audit log: each entry's SHA-256 covers the one before, so changing, removing or reordering any past entry is found; for admin actions, payments and consent records.//! version: 1.0.0//!//! [dependencies]//! sha2 = "0.10"use serde_json::{json, Map, Value};use sha2::{Digest, Sha256};const GENESIS: &str = "0000000000000000000000000000000000000000000000000000000000000000";/// The value with its maps' keys sorted, all the way down, so the same entry always hashes the same.fn canonical(v: &Value) -> Value { match v { Value::Object(m) => { let mut keys: Vec<&String> = m.keys().collect(); keys.sort(); let mut out = Map::new(); for k in keys { out.insert(k.clone(), canonical(&m[k])); } Value::Object(out) } Value::Array(a) => Value::Array(a.iter().map(canonical).collect()), other => other.clone(), }}fn digest(index: &Value, at: &Value, data: &Value, prev: &Value) -> String { let body = canonical(&json!({ "index": index, "at": at, "data": data, "prev": prev })); // Keys in order, written without spaces: the same text in any language that sorts keys. let text = serde_json::to_string(&body).unwrap_or_default(); Sha256::digest(text.as_bytes()).iter().map(|b| format!("{:02x}", b)).collect()}fn now_ms() -> i64 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_millis() as i64)}/// The next entry for `chain` (the entries so far, oldest first; `[]` for a new log) holding `data`: `{ index, at, data, prev,/// hash }`, `at` in milliseconds since 1970, `prev` the last entry's hash (64 zeros for the first). Store it as it is, after/// the others; only its own fields and the last entry are read, so a long log can pass only its last entry.pub fn hash_chain_append(chain: Vec<serde_json::Value>, data: serde_json::Value) -> Result<Value, String> { let (index, prev) = match chain.last() { None => (0, GENESIS.to_string()), Some(last) => { let i = last["index"].as_i64().ok_or("the last entry has no index: is this a chain hash_chain_append made?")?; let h = last["hash"].as_str().ok_or("the last entry has no hash")?; (i + 1, h.to_string()) } }; let at = now_ms(); let hash = digest(&json!(index), &json!(at), &data, &json!(prev)); Ok(json!({ "index": index, "at": at, "data": data, "prev": prev, "hash": hash }))}/// Checks a whole chain, oldest first: `{ ok, length, broken_at, reason }`. `broken_at` is the index of the first entry/// that was changed, removed, put in another place or added in the middle, null when all is well. A chain cut at its end/// still checks: keep the last hash elsewhere (another database, a daily email) to find that.pub fn hash_chain_verify(chain: Vec<serde_json::Value>) -> Value { let mut prev = GENESIS.to_string(); for (i, e) in chain.iter().enumerate() { let fail = |reason: String| json!({ "ok": false, "length": chain.len(), "broken_at": i, "reason": reason }); if e["index"].as_i64() != Some(i as i64) { return fail(format!("entry {} has index {}: one was removed or moved", i, e["index"])); } if e["prev"].as_str() != Some(prev.as_str()) { return fail(format!("entry {} does not follow the one before it", i)); } let h = digest(&e["index"], &e["at"], &e["data"], &e["prev"]); if e["hash"].as_str() != Some(h.as_str()) { return fail(format!("entry {} was changed after it was written", i)); } prev = h; } json!({ "ok": true, "length": chain.len(), "broken_at": null, "reason": null })}/// The hash an entry should have, worked out again from its `index`, `at`, `data` and `prev`: to check one entry alone.pub fn hash_chain_hash(entry: serde_json::Value) -> String { digest(&entry["index"], &entry["at"], &entry["data"], &entry["prev"])}Related
ct_logs.qwer · 1.0.0Read the SCTs in a certificate and check them against each log's key, ask a log for tree heads, entries and proofs, and verify Merkle audit and consistency proofs, so a certificate is shown to be logged and a log not to have rewritten history.5 views · 7 downloadsHashes hashes.qwer · 1.0.0MD5, SHA-1, SHA-2, SHA-3, BLAKE2, xxHash and CRC-32 of text and files, and Subresource Integrity values for script tags.7 views · 6 downloadsIP anonymizing ip_anonymize.qwer · 1.0.0IP addresses made GDPR-friendly: the last part cut off (/24, /48) for logs, or a salted hash that counts visitors without keeping them.7 views · 8 downloadsPII redaction pii_redact.qwer · 1.0.0Find and mask personal data in text before logs or outside services see it: emails, phones, card numbers, IBANs and IP addresses.8 views · 8 downloadsSecrets scanner secrets_scan.qwer · 1.0.0API keys, tokens, private keys, JWTs and passwords in URLs found in text by gitleaks-style rules, with documentation placeholders told apart, and the text given back with them masked.7 views · 8 downloadsMerkle trees merkle.qwer · 1.0.0Merkle trees as Certificate Transparency makes them: a root for a list or a file, a short proof that one entry is in it, and a proof that a log only grew.6 views · 7 downloadsThread 0 posts
On Hash chain: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.