Attestation
WCP defines two levels of attestation: registry record attestation (§5.10 artifact hash) and full-package attestation (worker code + deps + manifest). Both use SHA-256 and deterministic serialization. Both detect modification after the fact.
The Unit of Trust
The unit of attestation in WCP is the complete worker package — not just the code file. A worker package includes code, dependencies, configuration schema, and a signed manifest. All of it is hashed together.
worker-package/ code/ worker_logic.py bootstrap.py requirements.lock config.schema.json manifest.json ← signed manifest (excluded from hash input)Trust is bound to namespace-key authorization, not personal authorship. The trust statement embedded in every manifest reads:
Package attested by namespace <ns> at <UTC>; package hash sha256:<hash>.The Supply Chain Threat
Without attestation, the following attack is silent:
1. Attacker gains access to worker package after enrollment2. Attacker modifies worker_logic.py to exfiltrate payloads3. Worker retains original worker_id and capabilities4. Hall dispatches it normally — valid capability ID, valid tenant5. Data exfiltrated. Evidence receipt shows nothing wrong. Attack is invisible.Full-package attestation closes this attack surface at runtime. The moment any file in the package diverges from what was manifested, the next verification call fails closed.
Registry Record Attestation
Every registry record carries an artifact_hash — a SHA-256 over all other fields, serialized deterministically. This detects tampering with the enrollment document itself.
import hashlibimport json
def compute_artifact_hash(record: dict) -> str: record_without_hash = {k: v for k, v in record.items() if k != 'artifact_hash'} canonical = json.dumps(record_without_hash, sort_keys=True, separators=(',', ':')) return 'sha256:' + hashlib.sha256(canonical.encode()).hexdigest()This logic runs internally inside the Hall server (hall_api/server.py) at enrollment time. It is not a public import from pyhall.registry — do not call it directly. If you need to pre-compute the hash before enrolling, use the same pattern shown above inline.
At enrollment, the Hall re-computes the hash and compares it to the submitted artifact_hash field. A mismatch means the record was modified after it was generated — enrollment is rejected.
Full-Package Attestation
Full-package attestation is implemented by PackageAttestationVerifier. It verifies the entire worker package at runtime before execution.
Deny Codes
PackageAttestationVerifier is fail-closed. Any check failure returns a deny code with no silent fallback:
| Code | Cause |
|---|---|
ATTEST_MANIFEST_MISSING | manifest.json does not exist or is unreadable |
ATTEST_MANIFEST_ID_MISMATCH | Manifest worker_id / worker_species_id does not match the declared worker identity |
ATTEST_HASH_MISMATCH | Recomputed package hash does not match manifest.package_hash |
ATTEST_SIGNATURE_MISSING | No signature in manifest, or WCP_ATTEST_HMAC_KEY env var is not set |
ATTEST_SIG_INVALID | HMAC-SHA256 signature does not verify |
ATTEST_BANNED_HASH | Package hash matches a banned hash in the registry’s banned_hashes set |
All six deny codes are exported from pyhall directly:
from pyhall import ( PackageAttestationVerifier, ATTEST_MANIFEST_MISSING, ATTEST_MANIFEST_ID_MISMATCH, ATTEST_HASH_MISMATCH, ATTEST_SIGNATURE_MISSING, ATTEST_SIG_INVALID, ATTEST_BANNED_HASH,)canonical_package_hash
canonical_package_hash(package_root) computes a deterministic SHA-256 over the full package content. Hash input is one record per file, sorted lexicographically by relative POSIX path:
<relative_posix_path>\n<size_bytes>\n<sha256_hex(file_content)>\nFiles excluded from the hash: manifest.json, manifest.sig, manifest.tmp, .git/, __pycache__/, .DS_Store, *.pyc.
from pathlib import Pathfrom pyhall import canonical_package_hash
pkg_hash = canonical_package_hash(Path("/opt/workers/my-worker"))# returns a 64-character lowercase hex SHA-256 digestbuild_manifest and write_manifest
build_manifest() computes the canonical package hash, assembles the manifest dict, and signs it with HMAC-SHA256. write_manifest() writes the signed manifest to disk.
from pathlib import Pathfrom pyhall import build_manifest, write_manifest
manifest = build_manifest( package_root=Path("./my-worker"), worker_id="org.acme.summarizer.instance-1", worker_species_id="wrk.doc.summarizer", worker_version="1.0.0", signing_secret="your-namespace-hmac-key", # or read from env build_source="ci", # "local" | "ci" | "agent")
write_manifest(manifest, Path("./my-worker/manifest.json"))The signing secret is the namespace key. Set it via the WCP_ATTEST_HMAC_KEY environment variable in production. The manifest includes a trust_statement field with the canonical namespace-key trust claim.
scaffold_package
scaffold_package() creates a minimal worker package directory layout with stub files:
from pathlib import Pathfrom pyhall import scaffold_package
scaffold_package( package_root=Path("./my-worker"), worker_logic_file=Path("./my_logic.py"), # optional — stub written if None overwrite=False,)Layout created:
my-worker/ code/ bootstrap.py worker_logic.py requirements.lock config.schema.jsonAfter scaffolding, add your logic to code/worker_logic.py, pin dependencies in requirements.lock, then run build_manifest to sign the package.
PackageAttestationVerifier
PackageAttestationVerifier verifies a worker package at runtime. Use it at worker startup — before any execution — to confirm the package is attested and unchanged.
from pathlib import Pathfrom pyhall import PackageAttestationVerifier
verifier = PackageAttestationVerifier( package_root=Path("/opt/workers/my-worker"), manifest_path=Path("/opt/workers/my-worker/manifest.json"), worker_id="org.acme.summarizer.instance-1", worker_species_id="wrk.doc.summarizer", # secret_env defaults to "WCP_ATTEST_HMAC_KEY")
ok, deny_code, meta = verifier.verify()if not ok: raise SystemExit(f"Attestation denied: {deny_code}")
# On success, meta contains:# meta["trust_statement"] — canonical namespace-key trust claim# meta["package_hash"] — verified hash for embedding in evidence receipts# meta["verified_at_utc"] — UTC ISO 8601# meta["attested_at_utc"] — when the manifest was builtThe verification sequence:
manifest.jsonmust exist and be parseable →ATTEST_MANIFEST_MISSING- Manifest
worker_idandworker_species_idmust match the declared identity →ATTEST_MANIFEST_ID_MISMATCH - Recomputed
canonical_package_hashmust matchmanifest.package_hash→ATTEST_HASH_MISMATCH; if the hash matches a banned hash in the registry’s banned_hashes set →ATTEST_BANNED_HASH signature_hmac_sha256must be present andWCP_ATTEST_HMAC_KEYmust be set →ATTEST_SIGNATURE_MISSING- HMAC signature must verify →
ATTEST_SIG_INVALID
All checks must pass. No silent fallback.
Registry Record Verification
import hashlibimport json
def compute_artifact_hash(record: dict) -> str: record_without_hash = {k: v for k, v in record.items() if k != 'artifact_hash'} canonical = json.dumps(record_without_hash, sort_keys=True, separators=(',', ':')) return 'sha256:' + hashlib.sha256(canonical.encode()).hexdigest()
record = { "worker_id": "org.acme.summarizer", "worker_species_id": "wrk.doc.summarizer", "capabilities": ["cap.doc.summarize"], # ... all other fields}record["artifact_hash"] = compute_artifact_hash(record)
# Verify: recompute and compareexpected = compute_artifact_hash(record)if expected != record["artifact_hash"]: raise ValueError("Registry record has been tampered with")import { subtle } from 'crypto';
async function computeArtifactHash(record: Record<string, unknown>): Promise<string> { const { artifact_hash: _, ...withoutHash } = record; const sorted = JSON.stringify(withoutHash, Object.keys(withoutHash).sort()); const canonical = JSON.stringify(JSON.parse(sorted)); const data = new TextEncoder().encode(canonical); const hashBuffer = await subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); return 'sha256:' + hashHex;}cat registry_record.json \ | jq 'del(.artifact_hash)' \ | jq -cS . \ | openssl dgst -sha256 -hex \ | awk '{print "sha256:" $2}'Compliance Requirements
Attestation is off by default (require_worker_attestation: false). WCP-Full compliance requires it enabled in production:
| Setting | Default | WCP-Full production requirement |
|---|---|---|
require_worker_attestation | false | true |
require_signatory | false | true |
Both work together: signatory validation proves who is requesting; full-package attestation proves the worker that executes is exactly what was enrolled. Combined with the cryptographic evidence receipt and correlation chain, they establish an auditable chain of custody that survives post-incident review.