Initialize repository snapshot
This commit is contained in:
123
tools/check_sentinel_contract_parity.py
Normal file
123
tools/check_sentinel_contract_parity.py
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
check_sentinel_contract_parity.py
|
||||
|
||||
Ensures Sentinel v1 contracts (docs) and verifier implementation stay aligned:
|
||||
- Every FailureCode is referenced by the verifier implementation.
|
||||
- Every FailureCode is referenced by the contract matrix.
|
||||
- The contract matrix does not reference unknown E_/W_ codes.
|
||||
|
||||
Usage:
|
||||
python3 tools/check_sentinel_contract_parity.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from sentinel_failure_codes import FailureCode, WarningCode
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
VERIFIER_PATH = REPO_ROOT / "tools" / "vm_verify_sentinel_bundle.py"
|
||||
CONTRACT_MATRIX_PATH = REPO_ROOT / "spec" / "SENTINEL_V1_CONTRACT_MATRIX.md"
|
||||
SEMANTICS_PATH = REPO_ROOT / "spec" / "SENTINEL_FAILURE_CODE_SEMANTICS.md"
|
||||
|
||||
|
||||
def _read_text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
|
||||
verifier = _read_text(VERIFIER_PATH)
|
||||
matrix = _read_text(CONTRACT_MATRIX_PATH)
|
||||
semantics = _read_text(SEMANTICS_PATH)
|
||||
|
||||
known_failure_values = {c.value for c in FailureCode}
|
||||
known_warning_values = {c.value for c in WarningCode}
|
||||
|
||||
# 1) Every FailureCode is referenced by the verifier code.
|
||||
for code in FailureCode:
|
||||
needle = f"FailureCode.{code.name}"
|
||||
if needle not in verifier:
|
||||
errors.append(f"Verifier does not reference {needle} ({code.value})")
|
||||
|
||||
# 1b) Every WarningCode is referenced by the verifier code.
|
||||
for code in WarningCode:
|
||||
needle = f"WarningCode.{code.name}"
|
||||
if needle not in verifier:
|
||||
errors.append(f"Verifier does not reference {needle} ({code.value})")
|
||||
|
||||
# 2) Every FailureCode value appears in the contract matrix.
|
||||
for code in FailureCode:
|
||||
if code.value not in matrix:
|
||||
errors.append(
|
||||
f"Contract matrix does not reference failure code {code.value}"
|
||||
)
|
||||
|
||||
# 2b) Every WarningCode value appears in the contract matrix.
|
||||
for code in WarningCode:
|
||||
if code.value not in matrix:
|
||||
errors.append(
|
||||
f"Contract matrix does not reference warning code {code.value}"
|
||||
)
|
||||
|
||||
# 3) Contract matrix does not contain unknown E_/W_ codes.
|
||||
referenced_failures = set(re.findall(r"\bE_[A-Z_]+\b", matrix))
|
||||
referenced_warnings = set(re.findall(r"\bW_[A-Z_]+\b", matrix))
|
||||
|
||||
unknown_failures = sorted(referenced_failures - known_failure_values)
|
||||
unknown_warnings = sorted(referenced_warnings - known_warning_values)
|
||||
|
||||
if unknown_failures:
|
||||
errors.append(
|
||||
"Contract matrix references unknown failure codes: "
|
||||
+ ", ".join(unknown_failures)
|
||||
)
|
||||
if unknown_warnings:
|
||||
errors.append(
|
||||
"Contract matrix references unknown warning codes: "
|
||||
+ ", ".join(unknown_warnings)
|
||||
)
|
||||
|
||||
# 4) Semantics doc must cover all codes.
|
||||
for code in FailureCode:
|
||||
if code.value not in semantics:
|
||||
errors.append(f"Failure code semantics doc does not reference {code.value}")
|
||||
|
||||
for code in WarningCode:
|
||||
if code.value not in semantics:
|
||||
errors.append(f"Warning code semantics doc does not reference {code.value}")
|
||||
|
||||
# 5) Semantics doc does not contain unknown E_/W_ codes.
|
||||
referenced_failures = set(re.findall(r"\bE_[A-Z_]+\b", semantics))
|
||||
referenced_warnings = set(re.findall(r"\bW_[A-Z_]+\b", semantics))
|
||||
|
||||
unknown_failures = sorted(referenced_failures - known_failure_values)
|
||||
unknown_warnings = sorted(referenced_warnings - known_warning_values)
|
||||
|
||||
if unknown_failures:
|
||||
errors.append(
|
||||
"Failure code semantics doc references unknown failure codes: "
|
||||
+ ", ".join(unknown_failures)
|
||||
)
|
||||
if unknown_warnings:
|
||||
errors.append(
|
||||
"Failure code semantics doc references unknown warning codes: "
|
||||
+ ", ".join(unknown_warnings)
|
||||
)
|
||||
|
||||
if errors:
|
||||
for e in errors:
|
||||
print(f"[FAIL] {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("[OK] Sentinel contract parity verified")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
99
tools/make_proofbundle_testvectors.py
Executable file
99
tools/make_proofbundle_testvectors.py
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate ProofBundle conformance test vectors from a known-good bundle.
|
||||
|
||||
Usage:
|
||||
python3 make_proofbundle_testvectors.py \\
|
||||
/root/work/vaultmesh/proofbundle-sample.json \\
|
||||
/root/work/vaultmesh/testvectors/proofbundle
|
||||
|
||||
This script takes a valid ProofBundle and creates:
|
||||
- proofbundle-valid.json (copy of original with normalized bundle_id)
|
||||
- proofbundle-tampered-body.json (timestamp modified without updating root_hash)
|
||||
- proofbundle-tampered-root.json (wrong root_hash in a receipt)
|
||||
- proofbundle-broken-chain.json (previous_hash mismatch)
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_bundle(path: Path) -> dict:
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_bundle(bundle: dict, path: Path) -> None:
|
||||
path.write_text(
|
||||
json.dumps(bundle, indent=2, sort_keys=True, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"[+] wrote {path}")
|
||||
|
||||
|
||||
def make_valid(bundle: dict) -> dict:
|
||||
"""Create a normalized copy as the valid reference."""
|
||||
out = copy.deepcopy(bundle)
|
||||
out["bundle_id"] = "pb-test-valid"
|
||||
return out
|
||||
|
||||
|
||||
def make_tampered_body(bundle: dict) -> dict:
|
||||
"""Tamper a receipt's timestamp without updating root_hash."""
|
||||
out = copy.deepcopy(bundle)
|
||||
out["bundle_id"] = "pb-test-tampered-body"
|
||||
receipts = out.get("chain", {}).get("receipts", [])
|
||||
if len(receipts) >= 2:
|
||||
# Modify the second receipt's timestamp (the root_hash will be wrong)
|
||||
receipts[1]["timestamp"] = "2099-01-01T00:00:00.000Z"
|
||||
return out
|
||||
|
||||
|
||||
def make_tampered_root(bundle: dict) -> dict:
|
||||
"""Replace a receipt's root_hash with a clearly wrong value."""
|
||||
out = copy.deepcopy(bundle)
|
||||
out["bundle_id"] = "pb-test-tampered-root"
|
||||
receipts = out.get("chain", {}).get("receipts", [])
|
||||
if receipts:
|
||||
# Tamper the last receipt's root_hash
|
||||
receipts[-1]["root_hash"] = "blake3:deadbeefdeadbeefdeadbeefdeadbeef"
|
||||
return out
|
||||
|
||||
|
||||
def make_broken_chain(bundle: dict) -> dict:
|
||||
"""Break the chain linkage via previous_hash mismatch."""
|
||||
out = copy.deepcopy(bundle)
|
||||
out["bundle_id"] = "pb-test-broken-chain"
|
||||
receipts = out.get("chain", {}).get("receipts", [])
|
||||
if len(receipts) >= 2:
|
||||
# Break linkage at receipt[1]
|
||||
receipts[1]["previous_hash"] = "blake3:badcafebadcafebadcafebadcafebad0"
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print(
|
||||
"Usage: make_proofbundle_testvectors.py INPUT_BUNDLE OUTPUT_DIR",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
src = Path(sys.argv[1])
|
||||
dest_dir = Path(sys.argv[2])
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
base = load_bundle(src)
|
||||
|
||||
save_bundle(make_valid(base), dest_dir / "proofbundle-valid.json")
|
||||
save_bundle(make_tampered_body(base), dest_dir / "proofbundle-tampered-body.json")
|
||||
save_bundle(make_tampered_root(base), dest_dir / "proofbundle-tampered-root.json")
|
||||
save_bundle(make_broken_chain(base), dest_dir / "proofbundle-broken-chain.json")
|
||||
|
||||
print(f"\n[OK] Generated 4 test vectors in {dest_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
110
tools/run_sentinel_testvectors.sh
Executable file
110
tools/run_sentinel_testvectors.sh
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
VERIFIER="$ROOT/tools/vm_verify_sentinel_bundle.py"
|
||||
TV_DIR="$ROOT/testvectors/sentinel"
|
||||
|
||||
if [[ ! -x "$VERIFIER" && ! -f "$VERIFIER" ]]; then
|
||||
echo "[ERROR] Verifier not found: $VERIFIER" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -d "$TV_DIR" ]]; then
|
||||
echo "[ERROR] Testvector directory not found: $TV_DIR" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
expected_exit_for() {
|
||||
case "$1" in
|
||||
black-box-that-refused) echo 0 ;;
|
||||
rollback-duplicate-seq) echo 1 ;;
|
||||
corruption-truncated-jsonl) echo 1 ;;
|
||||
revocation-used-after-revoke) echo 1 ;;
|
||||
integrity-size-mismatch) echo 1 ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
expected_code_for() {
|
||||
case "$1" in
|
||||
rollback-duplicate-seq) echo "E_SEQ_NON_MONOTONIC" ;;
|
||||
corruption-truncated-jsonl) echo "E_SCHEMA_INVALID" ;;
|
||||
revocation-used-after-revoke) echo "E_REVOKED_CAPABILITY_USED" ;;
|
||||
integrity-size-mismatch) echo "E_MANIFEST_HASH_MISMATCH" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
missing=0
|
||||
|
||||
for dir in "$TV_DIR"/*; do
|
||||
[[ -d "$dir" ]] || continue
|
||||
name="$(basename "$dir")"
|
||||
|
||||
expected_exit="$(expected_exit_for "$name")"
|
||||
if [[ -z "$expected_exit" ]]; then
|
||||
echo "[ERROR] Unknown testvector: $name" >&2
|
||||
missing=1
|
||||
continue
|
||||
fi
|
||||
|
||||
report1="$(mktemp)"
|
||||
report2="$(mktemp)"
|
||||
|
||||
set +e
|
||||
python3 "$VERIFIER" --bundle "$dir" --strict --report "$report1" >/dev/null 2>/dev/null
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [[ "$status" -ne "$expected_exit" ]]; then
|
||||
echo "[FAIL] $name: exit=$status expected=$expected_exit" >&2
|
||||
python3 "$VERIFIER" --bundle "$dir" --strict --report "$report1" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
failure_code="$(python3 -c "import json; p='$report1'; fc=json.load(open(p,'r',encoding='utf-8')).get('failure_code'); print('' if fc is None else fc)")"
|
||||
|
||||
if [[ "$expected_exit" -eq 0 ]]; then
|
||||
if [[ -n "$failure_code" ]]; then
|
||||
echo "[FAIL] $name: expected PASS but failure_code=$failure_code" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
expected_code="$(expected_code_for "$name")"
|
||||
if [[ -z "$expected_code" ]]; then
|
||||
echo "[ERROR] Missing EXPECT_CODE mapping for failing vector: $name" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "$failure_code" != "$expected_code" ]]; then
|
||||
echo "[FAIL] $name: failure_code=$failure_code expected=$expected_code" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determinism: same inputs -> byte-identical report.
|
||||
set +e
|
||||
python3 "$VERIFIER" --bundle "$dir" --strict --report "$report2" >/dev/null 2>/dev/null
|
||||
status2=$?
|
||||
set -e
|
||||
|
||||
if [[ "$status2" -ne "$expected_exit" ]]; then
|
||||
echo "[FAIL] $name: second run exit=$status2 expected=$expected_exit" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! cmp -s "$report1" "$report2"; then
|
||||
echo "[FAIL] $name: report is not deterministic" >&2
|
||||
diff -u "$report1" "$report2" | head -n 200 >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "$report1" "$report2"
|
||||
echo "[OK] $name"
|
||||
done
|
||||
|
||||
if [[ "$missing" -ne 0 ]]; then
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "[OK] All Sentinel testvectors verified"
|
||||
22
tools/sentinel_failure_codes.py
Normal file
22
tools/sentinel_failure_codes.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class FailureCode(str, Enum):
|
||||
SCHEMA_INVALID = "E_SCHEMA_INVALID"
|
||||
MANIFEST_HASH_MISMATCH = "E_MANIFEST_HASH_MISMATCH"
|
||||
MISSING_REQUIRED_FILE = "E_MISSING_REQUIRED_FILE"
|
||||
EVENT_HASH_MISMATCH = "E_EVENT_HASH_MISMATCH"
|
||||
CHAIN_DISCONTINUITY = "E_CHAIN_DISCONTINUITY"
|
||||
SEQ_NON_MONOTONIC = "E_SEQ_NON_MONOTONIC"
|
||||
ROOT_MISMATCH = "E_ROOT_MISMATCH"
|
||||
RANGE_MISMATCH = "E_RANGE_MISMATCH"
|
||||
CANON_VERSION_UNSUPPORTED = "E_CANON_VERSION_UNSUPPORTED"
|
||||
OVERSIZE_INPUT = "E_OVERSIZE_INPUT"
|
||||
REVOKED_CAPABILITY_USED = "E_REVOKED_CAPABILITY_USED"
|
||||
|
||||
|
||||
class WarningCode(str, Enum):
|
||||
FILE_NOT_IN_MANIFEST = "W_FILE_NOT_IN_MANIFEST"
|
||||
RANGE_ROOT_PARTIAL = "W_RANGE_ROOT_PARTIAL"
|
||||
1650
tools/vm_verify_sentinel_bundle.py
Normal file
1650
tools/vm_verify_sentinel_bundle.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user