Skip to content

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.

Terminal window
mkdir pack && cd pack && unzip -q ../pack.zip

Every member name must be a plain relative path (letters, digits, ., -, _, /). Reject the pack otherwise.

Else UNKNOWN SIGNER.

Terminal window
jq -r '.signatures[0].keyid' manifest.dsse.json # must equal the registration sheet
jq -r '.keys[] | select(.key_id == "<KEY ID>") | .public_key' keys.json | base64 -d > pub.raw
sha256sum pub.raw # must equal the same key ID
cc911e1df79b8b7b97229eb8c163c1405b581680af2cb940386f74437c7aa524
cc911e1df79b8b7b97229eb8c163c1405b581680af2cb940386f74437c7aa524 pub.raw

If 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:

Terminal window
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.bin

Else TAMPERED. The signed bytes are the DSSE pre-authentication encoding: DSSEv1 <len(type)> <type> <len(payload)> <payload>, lengths in decimal bytes, single spaces.

Terminal window
jq -r .payload manifest.dsse.json | base64 -d > payload.bin
cmp payload.bin manifest.json && echo identical # must be identical
T=application/vnd.in-toto+json
{ printf 'DSSEv1 %d %s %d ' ${#T} "$T" "$(wc -c < payload.bin | tr -d ' ')"; cat payload.bin; } > pae.bin
jq -r '.signatures[0].sig' manifest.dsse.json | base64 -d > sig.bin
{ printf '302a300506032b6570032100' | xxd -r -p; cat pub.raw; } > pub.der # raw key -> SPKI DER
openssl pkey -pubin -inform DER -in pub.der -out pub.pem
openssl pkeyutl -verify -pubin -inkey pub.pem -rawin -in pae.bin -sigfile sig.bin
sha256sum manifest.json # the pack fingerprint
identical
Signature Verified Successfully
33f9acb1d1132b67e38b5bd0b2c35123f5b2032a84f8a034be6604fc338815f1 manifest.json

The fingerprint matches the one the verifier and the appliance’s packs list print.

Else TAMPERED, naming the file.

Terminal window
jq -r '.subject[] | "\(.digest.sha256) \(.name)"' manifest.json > SHA256SUMS
sha256sum -c SHA256SUMS
checkpoint.txt: OK
coverage.csv: OK
entries/000000.jsonl: OK
exceptions.csv: OK
inventory.csv: OK
keys.json: OK
proofs/consistency.json: OK
proofs/inclusion/155.json: OK
...
proofs/range.json: OK
reviews.json: OK

Every 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.

Terminal window
sed -n '1,/^$/p' checkpoint.txt | sed '$d' > cp-text.bin
tail -1 checkpoint.txt | cut -d' ' -f3 | base64 -d > cp-sig-full.bin
tail -c 64 cp-sig-full.bin > cp-sig.bin
openssl pkeyutl -verify -pubin -inkey pub.pem -rawin -in cp-text.bin -sigfile cp-sig.bin
jq -c .predicate.log manifest.json # origin, tree_size and root_hash must equal lines 1-3
head -3 checkpoint.txt
Signature Verified Successfully
{"entries_from":0,"entries_to":181,"origin":"gigidlabs.com/appliance/DEV01","root_hash":"WQHNjvnRAxCGVYqbAizpIt2v6mD60suPkcokNrjkf0M=","tree_size":181}
gigidlabs.com/appliance/DEV01
181
WQHNjvnRAxCGVYqbAizpIt2v6mD60suPkcokNrjkf0M=

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.

Terminal window
cat entries/*.jsonl > entries.jsonl
sed -n '1p' entries.jsonl | tr -d '\n' > e.bin # entry 0
{ printf '00' | xxd -r -p; cat e.bin; } | sha256sum # its leaf hash
sha256sum e.bin # its plain hash
sed -n '2p' entries.jsonl | jq -r .prev_hash # entry 1 must point at it
e06cc1b218a3fa9d8883696402989d57ed8cf0ac24bddb8e1565ea06dcc95d65 -
eb11bcd5d218492223a83d64829f62591e9d2e008c30a22b13b655eb9c218a4e e.bin
eb11bcd5d218492223a83d64829f62591e9d2e008c30a22b13b655eb9c218a4e

For 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.

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.

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.

Terminal window
jq -c '.predicate.counts' manifest.json
wc -l < entries.jsonl
grep -c '"type":"decision"' entries.jsonl
cut -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}
181
9
8 approved
25 needs_review
1 not_approved

Then 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.

Each bulk sign-off must agree with its members. On a real pack:

Terminal window
B=B-F4T5-PVTF
cat 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 . | sha256sum
cat 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.

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.