Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions plugin/hermes/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1456,6 +1456,10 @@ def tool_jackal_verify_bundle(args: dict[str, Any]) -> dict[str, Any]:
if not isinstance(args[key], str) or not args[key]:
raise PluginRefusal("plugin-args-schema",
f"missing/invalid field: {key!r}")
if not re.fullmatch(r"0|[1-9][0-9]*", args["verification_time_unix"]):
raise PluginRefusal(
"plugin-args-schema",
"verification_time_unix must be a nonnegative integer token")
verifier_path, verifier_pin = _claim_component("claim_verifier")
inf_path, inf_pin = _claim_component("claim_inference_registry")
unit_path, unit_pin = _claim_component("claim_unit_registry")
Expand Down
16 changes: 14 additions & 2 deletions release/claim/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ decision, display`
- `freshness` — `{"source_version","emitted_at_unix","max_age_seconds",
"expires_at_unix","nonce","environment_epoch"}` (nullable fields
explicit `null`). `environment_epoch` = pinned evaluator SHA-256.
Chronology is bounded on BOTH sides against the caller-supplied
verification time `t`: the verifier refuses `emitted_at_unix < 0` or
`t < 0` (`freshness-schema`), `expires_at_unix < 0` or
`expires_at_unix < emitted_at_unix` (`freshness-schema`),
`t < emitted_at_unix` (`freshness-premature`), `t > expires_at_unix`
when set (`freshness-expired`), and `t - emitted_at_unix >
max_age_seconds` when set (`freshness-stale`). These bind
SELF-DECLARED timestamps for lifecycle consistency only — see §10.
- `decision` — `null` or `{"decision_id","action","comparison","threshold",
"margin","consequence_class"}` (only `robust_decision` nodes).
- `display` — `{"text": "<untrusted commentary>"}` (may be empty string).
Expand Down Expand Up @@ -366,8 +374,12 @@ result, and the recomputed rendering token + permitted text.
verifier binary unavailable), never for semantic failure.

Replay honesty: the verifier proves nonce/epoch binding and caller-time
freshness only. One-time replay PREVENTION requires a durable external
nonce store and is an explicit residual non-claim in v1.
freshness only. Freshness bounds emission, verification, and expiry
against the caller-supplied time (see §3), but those timestamps are
SELF-DECLARED by the producer: the verifier enforces internal lifecycle
consistency, NOT timestamp authenticity or temporal provenance (there is
no trusted time source). One-time replay PREVENTION requires a durable
external nonce store and is an explicit residual non-claim in v1.

## 11. Renderer

Expand Down
2 changes: 1 addition & 1 deletion release/evidence/ci_claim_fixture_v160/pins.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"note": "caller-side semantic + infrastructure pins recorded at fixture creation; the recorded environment epoch is the sealed macOS arm64 jackal-native identity whose exact-cert output this bundle embeds - CI replays the recorded epoch, it does not rebuild the engine",
"root": "767334ad2c9340c78573e19c9b31b3968054eb08440a31b8f6621a6f29dcf8da",
"schema": "jackal-ci-claim-admission-pins-v1",
"verification_time_unix": 1786752000
"verification_time_unix": 1786924800
}
56 changes: 55 additions & 1 deletion release/evidence/claim_hostile_matrix_v160.json
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,60 @@
"observed": "verified/",
"ok": true
},
{
"expect": "refused/freshness-premature",
"id": "F-premature-future-emit",
"observed": "refused/freshness-premature",
"ok": true
},
{
"expect": "refused/freshness-schema",
"id": "F-negative-vtime",
"observed": "refused/freshness-schema",
"ok": true
},
{
"expect": "refused/freshness-schema",
"id": "F-expires-before-emit",
"observed": "refused/freshness-schema",
"ok": true
},
{
"expect": "refused/freshness-schema",
"id": "F-expires-negative",
"observed": "refused/freshness-schema",
"ok": true
},
{
"expect": "verified",
"id": "F-emit-eq-vtime-ok",
"observed": "verified/",
"ok": true
},
{
"expect": "verified",
"id": "F-expiry-eq-emit-eq-vtime-ok",
"observed": "verified/",
"ok": true
},
{
"expect": "verified",
"id": "F-aba-a1",
"observed": "verified/",
"ok": true
},
{
"expect": "refused/freshness-premature",
"id": "F-aba-b",
"observed": "refused/freshness-premature",
"ok": true
},
{
"expect": "verified",
"id": "F-aba-a2",
"observed": "verified/",
"ok": true
},
{
"expect": "verified",
"id": "F-pos-nonce",
Expand Down Expand Up @@ -652,5 +706,5 @@
],
"schema": "jackal-claim-hostile-matrix-v1",
"verdict": "PASS",
"verifier": "e0fcb9540c730bd9bb492b528ed42d29d49fc775b3aa0f9b831b6264fd68fd22"
"verifier": "90d6480e7353730488c88e1848972c837dc0e443e9a8e358327843e7ac13afc2"
}
60 changes: 60 additions & 0 deletions tests/claim_hostile_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,7 @@ def family_freshness() -> None:
bundle_of([n], n["id"]), "environment-mismatch")

n = input_node("x", "1", "2")
n["freshness"]["emitted_at_unix"] = str(VTIME - 1000)
n["freshness"]["expires_at_unix"] = str(VTIME - 10)
rehash(n)
expect_refused("F-expired",
Expand All @@ -1064,6 +1065,65 @@ def family_freshness() -> None:
rehash(n)
expect_verified("F-pos-age-ok", bundle_of([n], n["id"]))

# ---- chronology lower bounds (SIGNOFF proposal): the verifier must
# reject verification-before-emission, negative verification time,
# and contradictory emitted/expires lifecycles -- not only upper
# age/expiry bounds. RED on pre-fix bytes: the false-accept rows
# below VERIFY (F-expires-negative pre-fix refused under the wrong
# class) because only one-sided bounds were enforced.
n = input_node("x", "1", "2")
n["freshness"]["emitted_at_unix"] = str(VTIME + 100)
rehash(n)
expect_refused("F-premature-future-emit",
bundle_of([n], n["id"]), "freshness-premature")

n = input_node("x", "1", "2")
rehash(n)
expect_refused("F-negative-vtime",
bundle_of([n], n["id"]), "freshness-schema", vtime=-1)

n = input_node("x", "1", "2")
n["freshness"]["emitted_at_unix"] = str(VTIME)
n["freshness"]["expires_at_unix"] = str(VTIME - 100)
rehash(n)
expect_refused("F-expires-before-emit",
bundle_of([n], n["id"]), "freshness-schema",
vtime=VTIME - 100)

n = input_node("x", "1", "2")
n["freshness"]["expires_at_unix"] = "-5"
rehash(n)
expect_refused("F-expires-negative",
bundle_of([n], n["id"]), "freshness-schema")

n = input_node("x", "1", "2")
n["freshness"]["emitted_at_unix"] = str(VTIME)
rehash(n)
expect_verified("F-emit-eq-vtime-ok", bundle_of([n], n["id"]),
vtime=VTIME)

n = input_node("x", "1", "2")
n["freshness"]["emitted_at_unix"] = str(VTIME)
n["freshness"]["expires_at_unix"] = str(VTIME)
rehash(n)
expect_verified("F-expiry-eq-emit-eq-vtime-ok",
bundle_of([n], n["id"]), vtime=VTIME)

# A -> B -> A restoration on the chronology gate.
n = input_node("x", "1", "2")
n["freshness"]["emitted_at_unix"] = str(VTIME)
rehash(n)
expect_verified("F-aba-a1", bundle_of([n], n["id"]), vtime=VTIME)
n = input_node("x", "1", "2")
n["freshness"]["emitted_at_unix"] = str(VTIME + 100)
rehash(n)
expect_refused("F-aba-b", bundle_of([n], n["id"]),
"freshness-premature", vtime=VTIME)
n = input_node("x", "1", "2")
n["freshness"]["emitted_at_unix"] = str(VTIME)
rehash(n)
expect_verified("F-aba-a2", bundle_of([n], n["id"]), vtime=VTIME)

n = input_node("x", "1", "2")
n["freshness"]["nonce"] = "abc123"
rehash(n)
Expand Down
31 changes: 27 additions & 4 deletions tools/claim_bundle_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@
"environment-mismatch", "epoch-mismatch", "evidence-encoding",
"evidence-hash-mismatch", "evidence-kind", "evidence-producer-untrusted",
"evidence-prop-mismatch", "evidence-verify-failed", "float-forbidden",
"freshness-expired", "freshness-schema", "freshness-stale",
"freshness-expired", "freshness-premature", "freshness-schema",
"freshness-stale",
"graph-cycle", "implementation-upgrade", "int-budget",
"interval-div-zero", "interval-endpoints", "legacy-dispatch-failed",
"legacy-verifier-unavailable", "machine-cert-invalid",
Expand Down Expand Up @@ -2156,18 +2157,38 @@ def deep_validate_node(node: dict, units: UnitRegistry, args,
or max_age < 0):
raise Refusal("freshness-schema", "max_age_seconds")
expires = fresh["expires_at_unix"]
expires_val = None
if expires is not None:
parse_int_token(expires, what="expires_at")
expires_val = parse_int_token(expires, what="expires_at")
if expires_val < 0:
raise Refusal("freshness-schema", "expires_at negative")
if expires_val < emitted:
raise Refusal("freshness-schema",
f"expires_at {expires_val} precedes emitted_at "
f"{emitted}")
nonce = fresh["nonce"]
if nonce is not None and not isinstance(nonce, str):
raise Refusal("freshness-schema", "nonce")
if fresh["environment_epoch"] != engine_epoch:
raise Refusal("environment-mismatch",
f"node environment {str(fresh['environment_epoch'])[:16]}")
vtime = args.verification_time_unix
if expires is not None and vtime > int(expires):
# Chronology lower bounds: a well-formed bundle must not verify before
# it was emitted, at a negative (pre-epoch) verification time, or with a
# lifecycle whose expiry precedes emission. Ordering matters: domain
# violations refuse as freshness-schema; a valid-but-too-early check
# refuses as freshness-premature; only then are the upper expiry/age
# bounds evaluated (so vtime - emitted is always non-negative below).
if vtime < 0:
raise Refusal("freshness-schema",
f"verification time {vtime} is negative")
if vtime < emitted:
raise Refusal("freshness-premature",
f"verification time {vtime} precedes emitted_at "
f"{emitted}")
if expires_val is not None and vtime > expires_val:
raise Refusal("freshness-expired",
f"expired at {expires}, verification time {vtime}")
f"expired at {expires_val}, verification time {vtime}")
if max_age is not None and vtime - emitted > max_age:
raise Refusal("freshness-stale",
f"age {vtime - emitted}s > max {max_age}s")
Expand Down Expand Up @@ -2367,6 +2388,8 @@ def enforce_policy(policy: dict, bundle: dict, root: dict, by_id: dict,
if require["require_nonce"] and root["freshness"]["nonce"] is None:
raise Refusal("policy-violation", "nonce required by policy")
if require["max_age_seconds"] is not None:
# deep_validate_node has already enforced vtime >= emitted for every
# node (freshness-premature), so this policy age is non-negative.
emitted = int(root["freshness"]["emitted_at_unix"])
if args.verification_time_unix - emitted > \
require["max_age_seconds"]:
Expand Down
Loading