Verify a pack by hand
An examiner can check a pack without any GigID Labs software, using sha256sum (or shasum -a 256), base64, xxd, jq and an Ed25519-capable OpenSSL (version 3 or later). These are the published steps from internal/evidence/verify/SPEC.md, with the output of a real run on a pack exported in dev mode. Trust comes from one value you hold separately: the evidence key ID on the signed key registration sheet.
0. Unpack into an empty folder
Section titled “0. Unpack into an empty folder”mkdir pack && cd pack && unzip -q ../pack.zipEvery member name must be a plain relative path (letters, digits, ., -, _, /). Reject the pack otherwise.
1. The signing key is the registered key
Section titled “1. The signing key is the registered key”Else UNKNOWN SIGNER.
jq -r '.signatures[0].keyid' manifest.dsse.json # must equal the registration sheetjq -r '.keys[] | select(.key_id == "<KEY ID>") | .public_key' keys.json | base64 -d > pub.rawsha256sum pub.raw # must equal the same key IDcc911e1df79b8b7b97229eb8c163c1405b581680af2cb940386f74437c7aa524cc911e1df79b8b7b97229eb8c163c1405b581680af2cb940386f74437c7aa524 pub.rawIf the manifest is signed by a newer key, keys.json shows a handoff on it. Check that .old.key_id is the key you trust and .new.key_id the one that signed, then verify both signatures over the handoff body (the RFC 8785 JSON of the body without the two signatures, plus "statement":"gigidlabs.com/key-handoff/v1") as in step 2:
jq -jcS '.keys[] | select(.key_id == "<NEW KEY ID>") | .handoff.body | del(.old_signature, .new_signature) + {statement: "gigidlabs.com/key-handoff/v1"}' keys.json > handoff.bin2. The manifest signature is valid
Section titled “2. The manifest signature is valid”Else TAMPERED. The signed bytes are the DSSE pre-authentication encoding: DSSEv1 <len(type)> <type> <len(payload)> <payload>, lengths in decimal bytes, single spaces.
jq -r .payload manifest.dsse.json | base64 -d > payload.bincmp payload.bin manifest.json && echo identical # must be identicalT=application/vnd.in-toto+json{ printf 'DSSEv1 %d %s %d ' ${#T} "$T" "$(wc -c < payload.bin | tr -d ' ')"; cat payload.bin; } > pae.binjq -r '.signatures[0].sig' manifest.dsse.json | base64 -d > sig.bin{ printf '302a300506032b6570032100' | xxd -r -p; cat pub.raw; } > pub.der # raw key -> SPKI DERopenssl pkey -pubin -inform DER -in pub.der -out pub.pemopenssl pkeyutl -verify -pubin -inkey pub.pem -rawin -in pae.bin -sigfile sig.binsha256sum manifest.json # the pack fingerprintidenticalSignature Verified Successfully33f9acb1d1132b67e38b5bd0b2c35123f5b2032a84f8a034be6604fc338815f1 manifest.jsonThe fingerprint matches the one the verifier and the appliance’s packs list print.
3. Every file matches the manifest
Section titled “3. Every file matches the manifest”Else TAMPERED, naming the file.
jq -r '.subject[] | "\(.digest.sha256) \(.name)"' manifest.json > SHA256SUMSsha256sum -c SHA256SUMScheckpoint.txt: OKcoverage.csv: OKentries/000000.jsonl: OKexceptions.csv: OKinventory.csv: OKkeys.json: OKproofs/consistency.json: OKproofs/inclusion/155.json: OK...proofs/range.json: OKreviews.json: OKEvery file in the ZIP except manifest.json and manifest.dsse.json must be listed, and every listed file must be present.
4. The checkpoint is signed by the same key and matches the manifest
Section titled “4. The checkpoint is signed by the same key and matches the manifest”Else TAMPERED. The signed text is everything before the last blank line, including the newline after the last body line. The signature must be on a line whose name is exactly the origin on line 1. Its base64 decodes to 4 bytes of key hash followed by the 64-byte signature.
sed -n '1,/^$/p' checkpoint.txt | sed '$d' > cp-text.bintail -1 checkpoint.txt | cut -d' ' -f3 | base64 -d > cp-sig-full.bintail -c 64 cp-sig-full.bin > cp-sig.binopenssl pkeyutl -verify -pubin -inkey pub.pem -rawin -in cp-text.bin -sigfile cp-sig.binjq -c .predicate.log manifest.json # origin, tree_size and root_hash must equal lines 1-3head -3 checkpoint.txtSignature Verified Successfully{"entries_from":0,"entries_to":181,"origin":"gigidlabs.com/appliance/DEV01","root_hash":"WQHNjvnRAxCGVYqbAizpIt2v6mD60suPkcokNrjkf0M=","tree_size":181}gigidlabs.com/appliance/DEV01181WQHNjvnRAxCGVYqbAizpIt2v6mD60suPkcokNrjkf0M=5. Entries are in the signed log
Section titled “5. Entries are in the signed log”Else TAMPERED, naming the entry. Join the entries files first; the joined file must end in LF and hold no empty line. For an entry line E (without its newline): its leaf hash is SHA-256(0x00 ‖ E); its prev_hash equals the hex SHA-256 of the previous line; its seq is one more than the previous line’s.
cat entries/*.jsonl > entries.jsonlsed -n '1p' entries.jsonl | tr -d '\n' > e.bin # entry 0{ printf '00' | xxd -r -p; cat e.bin; } | sha256sum # its leaf hashsha256sum e.bin # its plain hashsed -n '2p' entries.jsonl | jq -r .prev_hash # entry 1 must point at ite06cc1b218a3fa9d8883696402989d57ed8cf0ac24bddb8e1565ea06dcc95d65 -eb11bcd5d218492223a83d64829f62591e9d2e008c30a22b13b655eb9c218a4e e.bineb11bcd5d218492223a83d64829f62591e9d2e008c30a22b13b655eb9c218a4eFor a cited entry, take proofs/inclusion/<seq>.json and run the RFC 9162 §2.1.3.2 procedure with leaf_index = seq, the checkpoint’s tree_size and interior nodes SHA-256(0x01 ‖ left ‖ right); the result must equal the checkpoint root.
The range proof (proofs/range.json) ties all the entries to the root; it is impractical by hand for large packs. Spot-check entries with their inclusion proofs and let the CLI do the range.
6. History is not rewritten
Section titled “6. History is not rewritten”Else HISTORY REWRITTEN or HISTORY NOT CHECKED. Every proof in proofs/consistency.json must pass the RFC 9162 §2.1.4.2 consistency check from its old_size and old_root to this pack’s checkpoint.
Then compare each checkpoint you hold from outside the appliance (the previous pack’s checkpoint.txt, or one your SIEM received). Check its signature as in step 4, under this pack’s origin. Same size: the roots must be equal. A size equal to an old_size in consistency.json: its root must equal that old_root. With no outside checkpoint the step is not checked, and the pack can still PASS; say so in the workpapers.
7. Totals match
Section titled “7. Totals match”Else INCONSISTENT. Recount and compare with predicate.counts and predicate.coverage: entries (lines of the entries files), decisions (entries with "type":"decision"), reviews, exceptions, devices by the status and tier columns of inventory.csv, and coverage rows by label. Coverage percent is covered source-days over covered, degraded and not-monitored source-days; before_install days are listed but left out.
jq -c '.predicate.counts' manifest.jsonwc -l < entries.jsonlgrep -c '"type":"decision"' entries.jsonlcut -d, -f3 inventory.csv | tail -n +2 | sort | uniq -c # quick count; status has no commas{"cited_entries":9,"decisions":9,"devices":{"approved":8,"excluded":0,"needs_review":25,"not_approved":1,"retired":0},"devices_by_tier":{"network_scan":0,"switch":34},"entries":181,"exceptions":1,"reviews":0} 1819 8 approved 25 needs_review 1 not_approvedThen check that each cited row agrees with its entry: each reviews.json row with its review_signed entry; each inventory.csv and exceptions.csv decision_seq with its decision entry; every discontinuity listed; every keys.json handoff byte for byte the body of its key_rotated entry.
Bulk sign-offs
Section titled “Bulk sign-offs”Each bulk sign-off must agree with its members. On a real pack:
B=B-F4T5-PVTFcat entries/*.jsonl | jq -c --arg b "$B" \ 'select((.type == "decision" or .type == "device_updated") and .body.batch_id? == $b) | {content_hash: .body.content_hash, device_id: .body.device_id, seq: .seq}' \ | jq -s -jcS . | sha256sumcat entries/*.jsonl | jq -c --arg b "$B" \ 'select(.type == "batch_signed" and .body.batch_id == $b) | .body | {count, member_seqs, members_hash}'ed9bf1ede762ec1f18c95acb88e2eab4d14f22ff57d22e5a42d864cc99a1586f -{"count":8,"member_seqs":[155,157,159,161,163,165,167,169],"members_hash":"ed9bf1ede762ec1f18c95acb88e2eab4d14f22ff57d22e5a42d864cc99a1586f"}The two hashes must match, the selected seqs must be exactly member_seqs, and count their number. A sign-off whose members start before the pack’s range cannot be checked from this pack; the verifier reports it as “not checked”, never as a failure.
Read the notices
Section titled “Read the notices”predicate.notices says what the pack cannot show by itself: “checkpoints not anchored”, “single-operator mode”, “N devices seen by network scan (point-in-time)” and “history discontinuity at entry N (reason)”. See Evidence pack format.