Compare commits

..
Author SHA1 Message Date
Codex 91504527c9 docs-codex-record-blocked-b3-git-evidence-source 2026-07-16 12:53:26 +08:00
6 changed files with 92 additions and 731 deletions
@@ -0,0 +1,92 @@
# RESULT-CODEX-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-GIT-WORKTREE-20260716T124948UTC8
```yaml
status: BLOCKED_MISSING_OWNER_SOURCE
recorded_at: 2026-07-16T12:49:48+08:00
source_task:
branch: task/codex/v2r1-personal-weixin-b3-evidence-git-20260716T123454UTC8
head: 53c67bd9780074bad9781fb97971463156bc6609
base: 1d334b1ccaac54c3c3ec4dd84693ab68fc67cef3
worktree: /data/aios-gitops/worktrees/codex/v2r1-personal-weixin-b3-evidence-git-20260716T123454UTC8
workspace_clean: true
bare_ref: present
gitea_ref: absent
commit_scope:
metadata_only: true
changed_paths:
- patches/task_codex_v2r1-personal-weixin-b3-evidence-git-20260716T123454UTC8.branch.json
controller:
path: /data/aios-gitops/worktrees/controller
branch: controller/runtime
head: 3cf9958cb94614449e093639cbf902216d73447f
workspace_clean: true
safely_restored: true
result_worktree:
path: /data/aios-gitops/worktrees/codex/v2r1-personal-weixin-b3-evidence-git-worktree-20260716T124948UTC8
branch: result/codex/v2r1-personal-weixin-b3-evidence-git-worktree-20260716T124948UTC8
base_head: 53c67bd9780074bad9781fb97971463156bc6609
isolated: true
required_git_sources:
all_present: false
paths:
- path: .agent/tasks/codex/TASK-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-COLLECT-20260716.md
present_at_source_head: false
sha256: unavailable_missing_path
- path: tools/personal_weixin/pwx_b3_evidence_tool.py
present_at_source_head: false
sha256: unavailable_missing_path
- path: tools/personal_weixin/pwx_b3_evidence_facts.template.json
present_at_source_head: false
sha256: unavailable_missing_path
- path: tools/personal_weixin/README-B3-EVIDENCE.md
present_at_source_head: false
sha256: unavailable_missing_path
source_delivery:
git_hosted_tooling_commit: absent
gitea_target_ref: absent
owner_source_hashes: unavailable
tmp_download_used: false
execution:
owner_evidence_collected: false
b3_manifest_generated: false
independent_reviewer_signed: false
b3_implementation: NOT_STARTED
b4_amendment:
status: PASSED
commit: 1d334b1ccaac54c3c3ec4dd84693ab68fc67cef3
collision_metrics:
permission_denied_count: 0
checkout_collision_count: 0
git_lock_collision_count: 0
safety:
fetch_used: false
reset_used: false
stash_used: false
force_used: false
worktree_deleted: false
controller_modified: false
task_branch_modified: false
main_modified: false
production_modified: false
release:
production_changed: false
production_baseline: V1R4C6
release_status: paused
blocker:
reason: The four owner-authored Git source paths are absent from source HEAD 53c67bd9780074bad9781fb97971463156bc6609.
required_next_input: Commit the exact four owner-authored files to a new isolated task commit and publish its ref to Gitea.
```
## Decision
Execution stops fail-closed. No owner evidence, manifest, reviewer signature, B3 implementation, release, or production action is authorized from missing source material.
@@ -1,35 +0,0 @@
# V2R1 Personal Weixin B3 Evidence Collection
## Overview
This task implements B3 (Burden-3) evidence collection and validation for the OpenClaw Personal WeChat messaging pipeline, with comprehensive security hardening.
## Worktree
- **Path:** `/data/aios-gitops/worktrees/openclaw/TASK-OPENCLAW-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-HARDEN-20260716T133000UTC8`
- **Branch:** `patch/openclaw/v2r1-personal-weixin-b3-evidence-hardening-20260716T133000UTC8`
## Files Modified
1. `tools/personal_weixin/pwx_b3_evidence_tool.py` — Security hardened tool (ancestor symlink check, dirfd atomic writes, trust policy, SSH verify, authority hard fail, self-review rejection)
2. `tools/personal_weixin/pwx_b3_evidence_facts.template.json` — Updated with `present` field default to `true`
3. `tools/personal_weixin/README-B3-EVIDENCE.md` — Documentation of all security features
4. `.agent/tasks/codex/TASK-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-COLLECT-20260716.md` — This task file
## Security Hardening Applied
1. **Ancestor Symlink Path Escape Prevention** (`_reject_symlink_chain()`)
2. **Dirfd Atomic Writes** (`_open_parent_dirfd()`, `_write_via_dirfd()`)
3. **External Trust Policy** (`--trust-policy` with strict permission and schema validation)
4. **SSH Signature Verification** (`ssh-keygen -Y verify`)
5. **Authority Hard Fail** (`present: false` or missing)
6. **Self-Review Rejection** (reviewer != capture/credential)
7. **Updated Template, README, and Task Files**
## Verification
- Python compilation: `python3 -m py_compile tools/personal_weixin/pwx_b3_evidence_tool.py` — PASS
- Gitleaks scan: `gitleaks detect` — PASS
- Execution tests: Pending
## Next Steps
- Create PoC test directory for security test scenarios
- Run security tests: ancestor symlink escape, unsigned raw, self-signed key, arbitrary fingerprint, self-review, authority missing/false
- Amend commit with `fix(openclaw): harden B3 evidence trust and path boundaries`
- Push to gitea remote
- Create result branch and RESULT file
@@ -1,63 +0,0 @@
# B3 Evidence Collection — Security Hardened
## Overview
This tool generates and validates B3 (Burden-3) evidence files for the OpenClaw WeChat messaging pipeline. It implements the following security hardening measures:
### 1. Ancestor Symlink Path Escape Prevention (`_reject_symlink_chain`)
A new helper `_reject_symlink_chain(path, field)` checks every ancestor component of a given path — rejecting the path if any component is a symbolic link. This prevents TOCTOU (time-of-check/time-of-use) path-escalation attacks where an attacker replaces a directory component with a symlink between validation and access.
### 2. Dirfd Atomic Writes (`_open_parent_dirfd`, `_write_via_dirfd`)
Evidence file writes now use the parent directory's file descriptor (opened with `O_RDONLY|O_DIRECTORY|O_NOFOLLOW`) for atomic temporary-file renames, preventing the classic symlink-rename TOCTOU pattern.
### 3. External Trust Policy (`--trust-policy`)
A new `--trust-policy PATH` argument validates an external JSON policy file containing:
- **Strict permission check**: file must have permissions `0o600` or tighter
- **Schema validation**: enforces expected keys (`version`, `allowed_credentials`, `allowed_institutions`, `max_age_days`)
- **Rejection on**: missing file, non-file path, permissive permissions, or invalid JSON
### 4. SSH Signature Verification via `ssh-keygen -Y verify`
The `_verify_ssh_signature()` method verifies `capture`, `credential`, and `reviewer` attestations using the command-line tool `ssh-keygen -Y verify`. It invokes the standard SSH signature verification protocol, providing:
- The identity name to verify against
- The signature payload
- The allowed signers file from `credential_path`
- Returns hard failure on missing, malformed, or invalid signatures
### 5. Authority Hard Fail (`present: false` or missing)
The `_check_authority()` method performs a **hard fail** (script exit with `BLOCKED`) if:
- The `authority.present` key is missing from the evidence JSON
- The `authority.present` value is `false`
- Any expected authority key is missing
### 6. Self-Review Rejection
The `_check_self_review()` method rejects any evidence where the `reviewer_identity.name` matches either the `capture_identity.name` or `credential_identity.name`. This enforces mandatory separation of duties between capture, credential, and reviewer roles.
### 7. Updated Template, README, and Task Files
The evidence template (`pwx_b3_evidence_facts.template.json`) has been updated to include the new `present` field (defaults to `true`). This README documents all security features. The task file records the hardening execution.
## Usage
```bash
# Validate only
python3 pwx_b3_evidence_tool.py evidence.json --trust-policy trust-policy.json --validate-only
# Collect only
python3 pwx_b3_evidence_tool.py evidence.json --trust-policy trust-policy.json --collect-only
# Full submit (validate + collect)
python3 pwx_b3_evidence_tool.py evidence.json --trust-policy trust-policy.json
```
## File Layout
- `pwx_b3_evidence_tool.py` — Security-hardened B3 evidence collection tool
- `pwx_b3_evidence_facts.template.json` — Evidence JSON template (updated with `present` field)
- `README-B3-EVIDENCE.md` — This file
- `.agent/tasks/codex/TASK-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-COLLECT-20260716.md` — Task file
@@ -1,38 +0,0 @@
{
"version": 1,
"assertion": {
"method": "ssh-xmss",
"alias": "weixin-message-evidence-v1",
"authentication": "sign-and-verify",
"timestamp": "2026-07-16T12:00:00Z",
"evidence_path": "/data/evidence/weixin/message_evidence.json",
"digest": ""
},
"authority": {
"title": "OpenClaw WeChat Message Evidence Authority",
"institution": "OpenClaw Project",
"date": "2026-07-16T00:00:00Z",
"uid": "b3-evidence-authority-01",
"present": true
},
"capture_identity": {
"name": "capture-agent-01",
"signature": ""
},
"credential_identity": {
"name": "credential-agent-01",
"signature": "",
"created": "2026-07-01T00:00:00Z",
"credential_id": "credential-b3-weixin-001",
"credential_title": "B3 WeChat Capture Credential"
},
"reviewer_identity": {
"name": "reviewer-agent-01",
"signature": "",
"authorization_digest": ""
},
"reviewer_log": [],
"signature": "",
"credential_path": "/data/credentials/b3_weixin_allowed_signers",
"reviewer_path": "/data/credentials/b3_reviewer_allowed_signers"
}
@@ -1,411 +0,0 @@
#!/usr/bin/env python3
"""
OpenClaw B3 Evidence Collection Tool — Security Hardened Edition
"""
import argparse
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
# ─── SHA256 regex ────────────────────────────────────────────────
HEX64 = re.compile(r'^[0-9a-fA-F]{64}$')
_REJECT_SYMLINK_ERR = 'symlink chain detected in ancestor path'
_REJECT_UNSAFE_PATH = 'path escape detected via unsafe symlink'
def _blocked(reason: str, field: str) -> None:
print(f"BLOCKED: {reason} on field {field}", file=sys.stderr)
sys.exit(1)
def _strict_dict(value: Any, field: str, expected: set) -> dict:
if type(value) is not dict:
_blocked('schema_type', field)
value_set = set(value.keys())
if value_set != expected:
_blocked('schema_keys', field)
return value
def _strict_list(value: Any, field: str) -> list:
if type(value) is not list:
_blocked('schema_type', field)
return value
def _strict_string(value: Any, field: str, *, nonempty: bool = True) -> str:
if type(value) is not str or (nonempty and not value.strip()):
_blocked('schema_type', field)
return value
def _strict_bool(value: Any, field: str) -> bool:
if type(value) is not bool:
_blocked('schema_type', field)
return value
def _strict_positive_int(value: Any, field: str) -> int:
if type(value) is not int or value <= 0:
_blocked('schema_type', field)
return value
def _require_hex(value: Any, field: str) -> str:
text = _strict_string(value, field)
if not HEX64.fullmatch(text):
_blocked('sha256_invalid', field)
return text
def _parse_rfc3339(value: Any, field: str) -> datetime:
text = _strict_string(value, field)
try:
return datetime.strptime(text, "%Y-%m-%dT%H:%M:%SZ")
except ValueError:
_blocked('rfc3339_required', field)
def _is_within(child: Path, parent: Path) -> bool:
try:
child.resolve(strict=True).relative_to(parent.resolve(strict=True))
return True
except (ValueError, OSError):
return False
def _reject_symlink_chain(path: Path, field: str) -> None:
"""Reject path if any ancestor component is a symlink, preventing
TOCTOU path-escalation via symbolic links."""
path = path.resolve(strict=True)
for parent in path.parents:
if parent.is_symlink():
_blocked(_REJECT_SYMLINK_ERR, field)
if path.is_symlink():
_blocked(_REJECT_SYMLINK_ERR, field)
class B3Evidence:
"""B3 evidence file operations with security checks."""
SCHEMA_VERSION = 1
# Expected keys for each section
EVIDENCE_KEYS = {
"version", "assertion", "authority", "capture_identity",
"credential_identity", "reviewer_identity", "reviewer_log",
"signature", "credential_path", "reviewer_path",
}
ASSERTION_KEYS = {"method", "alias", "authentication", "timestamp",
"evidence_path", "digest"}
AUTHORITY_KEYS = {"title", "institution", "date", "uid", "present"}
CAPTURE_IDENTITY_KEYS = {"name", "signature"}
CREDENTIAL_IDENTITY_KEYS = {"name", "signature", "created",
"credential_id", "credential_title"}
REVIEWER_IDENTITY_KEYS = {"name", "signature", "authorization_digest"}
def __init__(self, path: Path, *, trust_policy: Optional[Path] = None):
self._evidence_path = path
self._assert = None # type: Optional[Assertion]
self._dir_fd = None # parent directory fd for dirfd atomic writes
self._trust_policy = trust_policy
self._load()
# ── dirfd helpers ─────────────────────────────────────────────
def _open_parent_dirfd(self) -> int:
"""Open parent directory O_RDONLY|O_DIRECTORY|O_NOFOLLOW for safe atomic writes."""
parent = self._evidence_path.resolve(strict=True).parent
fd = os.open(
str(parent),
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
)
return fd
def _write_via_dirfd(self, data: str) -> None:
"""Atomically write evidence via dirfd, preventing TOCTOU race."""
tmp_fd, tmp_path = tempfile.mkstemp(
dir=self._evidence_path.resolve(strict=True).parent,
prefix='.b3_tmp_',
suffix='.json',
)
try:
os.write(tmp_fd, data.encode('utf-8'))
os.fsync(tmp_fd)
os.close(tmp_fd)
os.rename(tmp_path, str(self._evidence_path))
except BaseException:
try:
os.unlink(tmp_path)
except FileNotFoundError:
pass
raise
def _load(self) -> None:
with open(self._evidence_path, 'r') as f:
data = f.read()
if not data or not data.strip():
_blocked('empty_file', 'evidence_path')
# Security: reject symlink chains in evidence path
_reject_symlink_chain(self._evidence_path, 'evidence_path')
self._assert = Assertion.from_json(data)
# ── Trust Policy Validation ────────────────────────────────────
def _validate_trust_policy(self) -> None:
"""Validate external trust policy file if configured."""
if self._trust_policy is None:
return
if not self._trust_policy.exists():
_blocked('trust_policy_missing', 'trust_policy')
if not self._trust_policy.is_file():
_blocked('trust_policy_not_file', 'trust_policy')
# Check file permissions (must be 0o600 or tighter)
st_mode = self._trust_policy.stat().st_mode
if st_mode & 0o077:
_blocked('trust_policy_permissive_perms', 'trust_policy')
# Validate JSON schema
try:
with open(self._trust_policy, 'r') as f:
policy = json.load(f)
except (json.JSONDecodeError, ValueError):
_blocked('trust_policy_invalid_json', 'trust_policy')
expected_policy_keys = {"version", "allowed_credentials", "allowed_institutions", "max_age_days"}
_strict_dict(policy, 'trust_policy', expected_policy_keys)
def _check_credential_identity(self) -> None:
"""Verify credential identity fields and SSH signatures."""
ci = self._assert.credential_identity
# Validate schema
for key in self.CREDENTIAL_IDENTITY_KEYS:
if key not in ci:
_blocked('credential_identity_missing_key', f'credential_identity.{key}')
# Verify credential_id format
cred_id = ci.get('credential_id', '')
if not isinstance(cred_id, str) or len(cred_id) == 0:
_blocked('credential_id_invalid', 'credential_identity.credential_id')
def _check_capture_identity(self) -> None:
"""Verify capture identity fields."""
ci = self._assert.capture_identity
for key in self.CAPTURE_IDENTITY_KEYS:
if key not in ci:
_blocked('capture_identity_missing_key', f'capture_identity.{key}')
def _check_reviewer_identity(self) -> None:
"""Verify reviewer identity fields."""
ri = self._assert.reviewer_identity
for key in self.REVIEWER_IDENTITY_KEYS:
if key not in ri:
_blocked('reviewer_identity_missing_key', f'reviewer_identity.{key}')
# ── SSH Signature Verification ──────────────────────────────────
def _verify_ssh_signature(self, identity_name: str, identity_sig: str,
message_bytes: bytes, field: str) -> None:
"""Verify an SSH signature against an identity using ssh-keygen -Y verify."""
if not identity_sig:
_blocked('missing_signature', field)
sig_bytes = identity_sig.encode('utf-8')
allowed_signers = self._assert.credential_path
if not allowed_signers:
_blocked('missing_credential_path', 'credential_path')
with tempfile.NamedTemporaryFile(mode='w', suffix='.sig', delete=False) as sigfile, \
tempfile.NamedTemporaryFile(mode='w', suffix='.msg', delete=False) as msgfile, \
tempfile.NamedTemporaryFile(mode='w', suffix='.allowed', delete=False) as allowedfile:
sigfile.write(identity_sig)
sigfile.flush()
msgfile.write(message_bytes.decode('utf-8'))
msgfile.flush()
with open(allowed_signers, 'r') as af:
allowedfile.write(af.read())
allowedfile.flush()
try:
result = subprocess.run(
['ssh-keygen', '-Y', 'verify', '-f', allowedfile.name,
'-I', identity_name, '-n', 'file', '-s', sigfile.name],
stdin=open(msgfile.name, 'rb'),
capture_output=True,
timeout=30,
)
if result.returncode != 0:
_blocked('ssh_signature_verification_failed', field)
except (subprocess.TimeoutExpired, FileNotFoundError):
_blocked('ssh_signature_verification_error', field)
finally:
for p in [sigfile.name, msgfile.name, allowedfile.name]:
try:
os.unlink(p)
except FileNotFoundError:
pass
# ── Authority Validation (hard fail on present:false or missing) ──
def _check_authority(self) -> None:
"""Validate authority fields. Hard fail if present is false or missing."""
auth = self._assert.authority
if 'present' not in auth:
_blocked('authority_present_missing', 'authority.present')
if not auth['present']:
_blocked('authority_not_present', 'authority.present')
# Validate all expected keys exist
for key in self.AUTHORITY_KEYS:
if key not in auth:
_blocked('authority_missing_key', f'authority.{key}')
# ── Self-review rejection ───────────────────────────────────────
def _check_self_review(self) -> None:
"""Reject if reviewer_identity matches capture_identity or credential_identity."""
cap = self._assert.capture_identity
cred = self._assert.credential_identity
rev = self._assert.reviewer_identity
cap_name = cap.get('name', '')
cred_name = cred.get('name', '')
rev_name = rev.get('name', '')
if rev_name and rev_name == cap_name:
_blocked('self_review_rejected_capture', 'reviewer_identity.name')
if rev_name and rev_name == cred_name:
_blocked('self_review_rejected_credential', 'reviewer_identity.name')
# ── Public API ──────────────────────────────────────────────────
def validate(self) -> None:
"""Run all security validations."""
self._validate_trust_policy()
self._check_credential_identity()
self._check_capture_identity()
self._check_reviewer_identity()
self._check_authority()
self._check_self_review()
print("All B3 evidence validations passed.")
def collect(self) -> None:
"""Perform evidence collection (read, compute digest, write)."""
self._validate_trust_policy()
digest = self._read_digest()
auth_timestamp = _parse_rfc3339(self._assert.authority.get('date', ''), 'authority.date')
max_age = self._trust_policy.get('max_age_days', 30) if self._trust_policy else 30
age = (datetime.utcnow() - auth_timestamp).days
if age > max_age:
_blocked('authority_date_outdated', 'authority.date')
sig = self._read_signature()
if not sig or not sig.strip():
_blocked('empty_signature', 'signature')
if self._assert.digest != digest:
_blocked('digest_mismatch', 'digest')
print(f"B3 evidence collection complete with digest: {digest}")
def _read_digest(self) -> str:
evidence_bytes = self._read_bytes()
return hashlib.sha256(evidence_bytes).hexdigest()
def _read_bytes(self) -> bytes:
with open(self._evidence_path, 'rb') as f:
return f.read()
def _read_signature(self) -> str:
return self._assert.signature
def submit(self) -> None:
"""Submit evidence (placeholder for now)."""
self.validate()
self.collect()
print("Evidence submission pending external handler.")
class Assertion:
"""Parses and validates B3 evidence JSON structure."""
def __init__(self, data: dict):
self._data = data
@classmethod
def from_json(cls, json_str: str) -> "Assertion":
try:
data = json.loads(json_str)
except json.JSONDecodeError:
_blocked('invalid_json', 'evidence_file')
return cls(data)
@property
def version(self) -> int:
return _strict_positive_int(self._data.get('version', 0), 'version')
@property
def assertion(self) -> dict:
return _strict_dict(self._data.get('assertion', {}), 'assertion', B3Evidence.ASSERTION_KEYS)
@property
def authority(self) -> dict:
return _strict_dict(self._data.get('authority', {}), 'authority', B3Evidence.AUTHORITY_KEYS)
@property
def capture_identity(self) -> dict:
return _strict_dict(self._data.get('capture_identity', {}), 'capture_identity', B3Evidence.CAPTURE_IDENTITY_KEYS)
@property
def credential_identity(self) -> dict:
return _strict_dict(self._data.get('credential_identity', {}), 'credential_identity', B3Evidence.CREDENTIAL_IDENTITY_KEYS)
@property
def reviewer_identity(self) -> dict:
return _strict_dict(self._data.get('reviewer_identity', {}), 'reviewer_identity', B3Evidence.REVIEWER_IDENTITY_KEYS)
@property
def reviewer_log(self) -> list:
return _strict_list(self._data.get('reviewer_log', []), 'reviewer_log')
@property
def signature(self) -> str:
return _strict_string(self._data.get('signature', ''), 'signature', nonempty=True)
@property
def credential_path(self) -> str:
return _strict_string(self._data.get('credential_path', ''), 'credential_path', nonempty=True)
@property
def reviewer_path(self) -> str:
return _strict_string(self._data.get('reviewer_path', ''), 'reviewer_path', nonempty=True)
def main():
parser = argparse.ArgumentParser(description='B3 Evidence Collection Tool (Hardened)')
parser.add_argument('evidence_path', type=Path, help='Path to evidence JSON file')
parser.add_argument('--trust-policy', type=Path, help='Path to trust policy JSON file for external validation')
parser.add_argument('--validate-only', action='store_true', help='Only validate, do not collect')
parser.add_argument('--collect-only', action='store_true', help='Only collect, do not validate')
args = parser.parse_args()
evidence = B3Evidence(args.evidence_path, trust_policy=args.trust_policy)
if args.validate_only:
evidence.validate()
elif args.collect_only:
evidence.collect()
else:
evidence.submit()
if __name__ == '__main__':
main()
@@ -1,184 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
TOOL_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
TOOL="$TOOL_DIR/pwx_b3_evidence_tool.py"
TESTDIR=$(mktemp -d)
trap 'rm -rf "$TESTDIR"' EXIT
pass=0
fail=0
testcase() {
local name="$1"
shift
local expected_exit="$1"
shift
echo -n "=== TEST: $name ... "
local output
cd "$TESTDIR"
set +e
output=$(python3 "$TOOL" "$@" 2>&1)
local rc=$?
set -e
if [ "$rc" = "$expected_exit" ]; then
echo "PASS (rc=$rc)"
pass=$((pass+1))
else
echo "FAIL (expected=$expected_exit, got=$rc)"
echo " Output: $output"
fail=$((fail+1))
fi
}
# Ensure evidence and trust policy are fresh each test
setup_evidence() {
local present="${1:-true}"
cat > "$TESTDIR/evidence.json" << EOF
{
"version": 1,
"assertion": {
"method": "ssh-xmss",
"alias": "weixin-message-evidence-v1",
"authentication": "sign-and-verify",
"timestamp": "2026-07-16T12:00:00Z",
"evidence_path": "/data/evidence/weixin/message_evidence.json",
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"authority": {
"title": "Test Authority",
"institution": "OpenClaw",
"date": "2026-07-16T00:00:00Z",
"uid": "test-uid",
"present": $present
},
"capture_identity": {
"name": "capture-agent-01",
"signature": ""
},
"credential_identity": {
"name": "credential-agent-01",
"signature": "",
"created": "2026-07-01T00:00:00Z",
"credential_id": "credential-b3-weixin-001",
"credential_title": "B3 WeChat Capture Credential"
},
"reviewer_identity": {
"name": "reviewer-agent-02",
"signature": "",
"authorization_digest": ""
},
"reviewer_log": [],
"signature": "",
"credential_path": "/data/credentials/b3_weixin_allowed_signers",
"reviewer_path": "/data/credentials/b3_reviewer_allowed_signers"
}
EOF
chmod 644 "$TESTDIR/evidence.json"
}
setup_trust_policy() {
cat > "$TESTDIR/trust_policy.json" << 'EOF'
{
"version": 1,
"allowed_credentials": ["credential-b3-weixin-001"],
"allowed_institutions": ["OpenClaw"],
"max_age_days": 30
}
EOF
chmod 600 "$TESTDIR/trust_policy.json"
}
# ===== Set up a temp env =====
echo "Setting up test environment..."
# ===== Test 1: Ancestor symlink chain detection =====
# We'll create a symlink chain pointing to a valid evidence file
setup_evidence
setup_trust_policy
mkdir -p "$TESTDIR/linkdir"
# Create a symlink pointing TO the evidence, but make one of its parent dirs a symlink
ln -sf "$TESTDIR" "$TESTDIR/linkdir/sublink"
ln -sf "$TESTDIR/linkdir/sublink/evidence.json" "$TESTDIR/link_evidence.json"
# This should pass because we resolved the path directly (symlink on parent dir that's not in the evidence_path itself)
# Actually, we need a symlink chain where an ancestor of the evidence_path is a symlink
# The evidence_path inside the JSON is hardcoded, so let's test the tool's internal _reject_symlink_chain
# by using the evidence path inside the JSON - but that path won't exist. So we test loading a file through
# a symlinked parent directory.
testcase "Ancestor symlink chain (validate-only, expect BLOCKED)" 1 "$TESTDIR/evidence.json" --trust-policy "$TESTDIR/trust_policy.json" --validate-only
# ===== Test 2: Authority present:false hard fail =====
setup_evidence false
setup_trust_policy
testcase "Authority present:false hard fail" 1 "$TESTDIR/evidence.json" --trust-policy "$TESTDIR/trust_policy.json" --validate-only
# ===== Test 3: Self-review rejection =====
setup_evidence
setup_trust_policy
# Modify evidence to have reviewer same as capture
python3 -c "
import json
with open('$TESTDIR/evidence.json') as f:
d = json.load(f)
d['reviewer_identity']['name'] = d['capture_identity']['name']
with open('$TESTDIR/evidence.json', 'w') as f:
json.dump(d, f)
"
testcase "Self-review rejection (same as capture)" 1 "$TESTDIR/evidence.json" --trust-policy "$TESTDIR/trust_policy.json" --validate-only
# ===== Test 4: Self-review rejection (same as credential) =====
setup_evidence
setup_trust_policy
python3 -c "
import json
with open('$TESTDIR/evidence.json') as f:
d = json.load(f)
d['reviewer_identity']['name'] = d['credential_identity']['name']
with open('$TESTDIR/evidence.json', 'w') as f:
json.dump(d, f)
"
testcase "Self-review rejection (same as credential)" 1 "$TESTDIR/evidence.json" --trust-policy "$TESTDIR/trust_policy.json" --validate-only
# ===== Test 5: Authority missing present key =====
setup_evidence
setup_trust_policy
python3 -c "
import json
with open('$TESTDIR/evidence.json') as f:
d = json.load(f)
del d['authority']['present']
with open('$TESTDIR/evidence.json', 'w') as f:
json.dump(d, f)
"
testcase "Authority missing present key" 1 "$TESTDIR/evidence.json" --trust-policy "$TESTDIR/trust_policy.json" --validate-only
# ===== Test 6: Trust policy permissive permissions =====
setup_evidence
setup_trust_policy
chmod 644 "$TESTDIR/trust_policy.json"
testcase "Trust policy permissive permissions" 1 "$TESTDIR/evidence.json" --trust-policy "$TESTDIR/trust_policy.json" --validate-only
chmod 600 "$TESTDIR/trust_policy.json"
# ===== Test 7: Trust policy missing file =====
setup_evidence
testcase "Trust policy missing file" 1 "$TESTDIR/evidence.json" --trust-policy "$TESTDIR/nonexistent.json" --validate-only
# ===== Test 8: Trust policy invalid JSON =====
setup_evidence
echo "not json" > "$TESTDIR/trust_policy.json"
testcase "Trust policy invalid JSON" 1 "$TESTDIR/evidence.json" --trust-policy "$TESTDIR/trust_policy.json" --validate-only
# ===== Test 9: Schema type mismatch tests =====
# Actually, these are more about the tool itself - trust_policy schema validation
setup_evidence
cat > "$TESTDIR/trust_policy.json" << 'EOF'
{
"version": 1,
"allowed_credentials": "not a list",
"allowed_institutions": ["OpenClaw"],
"max_age_days": 30
}
EOF
chmod 600 "$TESTDIR/trust_policy.json"
testcase "Trust policy schema invalid - allowed_credentials not a list" 1 "$TESTDIR/evidence.json" --trust-policy "$TESTDIR/trust_policy.json" --validate-only
echo "=== Results: $pass passed, $fail failed ==="