Skip to content

Commit 7f5ed33

Browse files
Resync-the-published-TRACE-schema-and-guard-it
The file served at https://agentrust-io.com/schema/trace-v0.2.json is the one an implementation fetches, because that is the $id every TRACE record carries. It was published once and never updated, and had fallen four commits behind the normative copy in trace-spec. The drift was not cosmetic. The published copy was missing the allOf constraint that stops a record assembled from another party's evidence from claiming a hardware root, missing the origin property that constraint reads, and it required `transparency` where the specification does not. A validator using the canonical URL was therefore rejecting records the specification allows, at exactly the trust levels that have no transparency anchor. Resync it, and publish the two revocation schemas, which declare an agentrust-io.com $id and returned 404. trace-spec already guards its packaged SDK copy against its normative copy, after the same class of drift (agentrust-io/trace-spec#136). The published copy was a third copy that no check covered. Add that check here, where the artifact is served: every file under schema/ must match its source in trace-spec and must declare the $id it is actually served at. It runs on changes and daily, since upstream can move without anything happening in this repository. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent da45731 commit 7f5ed33

4 files changed

Lines changed: 420 additions & 29 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Schema parity
2+
3+
# The files under schema/ are published at the $id every TRACE record points at,
4+
# so an implementation fetching that URL is validated against whatever this repo
5+
# serves. The normative copies live in agentrust-io/trace-spec. Nothing linked
6+
# the two, and they drifted: the published v0.2 schema lost an allOf constraint
7+
# and an origin property, and required `transparency` where the spec did not, so
8+
# a validator using the canonical URL rejected records the spec allows.
9+
#
10+
# trace-spec already guards its packaged SDK copy against its normative copy
11+
# (agentrust-io/trace-spec#136). This is the same check for the published copy.
12+
13+
on:
14+
push:
15+
branches: [main]
16+
paths: ['schema/**']
17+
pull_request:
18+
paths: ['schema/**']
19+
schedule:
20+
# Upstream can change without anything happening here, so also check daily.
21+
- cron: '17 6 * * *'
22+
workflow_dispatch:
23+
24+
permissions:
25+
contents: read
26+
27+
jobs:
28+
parity:
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/checkout@v4
32+
33+
- name: Compare each published schema with its normative source
34+
run: |
35+
set -euo pipefail
36+
raw=https://raw.githubusercontent.com/agentrust-io/trace-spec/main/schema
37+
fail=0
38+
39+
# published file -> normative file in trace-spec
40+
check() {
41+
local published="$1" source="$2"
42+
if [ ! -f "$published" ]; then
43+
echo "::error::$published is missing"
44+
fail=1
45+
return
46+
fi
47+
if ! curl -fsSL "$raw/$source" -o /tmp/upstream.json; then
48+
echo "::error::could not fetch $raw/$source"
49+
fail=1
50+
return
51+
fi
52+
if diff -q \
53+
<(jq -S . "$published") \
54+
<(jq -S . /tmp/upstream.json) >/dev/null; then
55+
echo "ok $published"
56+
else
57+
echo "::error::$published has drifted from trace-spec/schema/$source"
58+
diff <(jq -S . "$published") <(jq -S . /tmp/upstream.json) || true
59+
fail=1
60+
fi
61+
}
62+
63+
check schema/trace-v0.2.json trace-claim.json
64+
check schema/trace-revocation-v1.json trace-revocation.json
65+
check schema/trace-revocation-bundle-v1.json trace-revocation-bundle.json
66+
67+
exit $fail
68+
69+
- name: Every published schema declares the $id it is served at
70+
run: |
71+
set -euo pipefail
72+
fail=0
73+
for f in schema/*.json; do
74+
want="https://agentrust-io.com/$f"
75+
got=$(jq -r '."$id" // empty' "$f")
76+
if [ "$got" != "$want" ]; then
77+
echo "::error::$f declares \$id '$got' but is served at '$want'"
78+
fail=1
79+
else
80+
echo "ok $f"
81+
fi
82+
done
83+
exit $fail
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://agentrust-io.com/schema/trace-revocation-bundle-v1.json",
4+
"title": "TRACE Revocation Bundle",
5+
"description": "A signed, cacheable set of TraceRevocation/1.0 statements with an explicit validity horizon. Spec trace-v0.2 section 3.2.3. The bundle exists so revocation does not require a callback at verification time: a verifier offline states what it checked against rather than skipping the check silently.",
6+
"type": "object",
7+
"required": [
8+
"type",
9+
"log_id",
10+
"issued_at",
11+
"valid_until",
12+
"statements",
13+
"bundle_key_id",
14+
"sig"
15+
],
16+
"additionalProperties": false,
17+
"properties": {
18+
"type": {
19+
"type": "string",
20+
"const": "TraceRevocationBundle/1.0"
21+
},
22+
"log_id": {
23+
"type": "string",
24+
"minLength": 1,
25+
"description": "The transparency log every statement in this bundle refers to. One log per bundle, so an entry-ID comparison can never be made across logs by accident."
26+
},
27+
"issued_at": {
28+
"type": "integer",
29+
"minimum": 1700000000,
30+
"description": "When the bundle was assembled, Unix epoch seconds."
31+
},
32+
"valid_until": {
33+
"type": "integer",
34+
"minimum": 1700000000,
35+
"description": "The horizon this bundle may be relied on to. Past it, a verifier reports the record as unverified for revocation rather than verified: an expired bundle is not evidence a key is still trusted, and spec section 3.2.3 forbids reporting it as an affirming appraisal."
36+
},
37+
"statements": {
38+
"type": "array",
39+
"description": "The revocation statements. An empty array is meaningful and legitimate: it asserts that as of issued_at the issuer knew of no revoked keys on this log, which is different from having no bundle at all.",
40+
"items": {
41+
"$ref": "https://agentrust-io.com/schema/trace-revocation-v1.json"
42+
}
43+
},
44+
"bundle_key_id": {
45+
"type": "string",
46+
"minLength": 1,
47+
"description": "The key signing this bundle. The bundle signature authenticates the set and its horizon; each statement inside stays independently signed, so a bundle assembler cannot add a revocation it was not authorised to issue."
48+
},
49+
"sig": {
50+
"type": "object",
51+
"required": ["alg", "value"],
52+
"additionalProperties": false,
53+
"description": "Signature over the RFC 8785 canonical form of this object with `sig` absent.",
54+
"properties": {
55+
"alg": {
56+
"type": "string",
57+
"enum": ["ed25519", "ES256", "ES384"]
58+
},
59+
"value": {
60+
"type": "string",
61+
"pattern": "^[A-Za-z0-9_-]+$",
62+
"description": "base64url, no padding."
63+
}
64+
}
65+
}
66+
}
67+
}

‎schema/trace-revocation-v1.json‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://agentrust-io.com/schema/trace-revocation-v1.json",
4+
"title": "TRACE Revocation Statement",
5+
"description": "A TraceRevocation/1.0 statement withdrawing a record-signing key from a transparency-log entry onward. Spec trace-v0.2 section 3.2.3. The boundary is a log entry ID rather than a timestamp: a compromised record-signing key also signs the iat it would be judged against, so any time-anchored rule is defeated by backdating.",
6+
"type": "object",
7+
"required": [
8+
"type",
9+
"compromised_key_id",
10+
"last_valid_entry_id",
11+
"log_id",
12+
"revocation_key_id",
13+
"sig"
14+
],
15+
"additionalProperties": false,
16+
"properties": {
17+
"type": {
18+
"type": "string",
19+
"const": "TraceRevocation/1.0",
20+
"description": "Claim type identifier."
21+
},
22+
"compromised_key_id": {
23+
"type": "string",
24+
"minLength": 1,
25+
"description": "The revoked record-signing key, as an RFC 7638 JWK thumbprint or a kid. Matching on either identifier rejects the record."
26+
},
27+
"last_valid_entry_id": {
28+
"type": "string",
29+
"minLength": 1,
30+
"description": "The highest log entry ID at which this key's records remain valid. A record whose inclusion entry ID is less than or equal to this value is unaffected; a greater one is rejected."
31+
},
32+
"revoked_after_entry": {
33+
"type": "string",
34+
"minLength": 1,
35+
"description": "The next entry ID after last_valid_entry_id. Redundant by construction and carried so a reader does not have to know the log's successor function to see where the boundary falls."
36+
},
37+
"log_id": {
38+
"type": "string",
39+
"minLength": 1,
40+
"description": "The transparency log the entry IDs refer to. Entry IDs from a different log are not comparable and must not satisfy the verifier rule."
41+
},
42+
"reason": {
43+
"type": "string",
44+
"description": "Why the key was revoked. Free text: the verifier rule does not branch on it, and constraining it would invite a false sense that it is machine-actionable.",
45+
"examples": [
46+
"key compromise",
47+
"superseded",
48+
"operator request"
49+
]
50+
},
51+
"revoked_at": {
52+
"type": "integer",
53+
"minimum": 1700000000,
54+
"description": "When the revocation was issued, Unix epoch seconds. Informational only. It is deliberately NOT the boundary: see the description of this schema."
55+
},
56+
"revocation_key_id": {
57+
"type": "string",
58+
"minLength": 1,
59+
"description": "The key signing this statement. Spec section 3.2.3 requires it to sit above compromised_key_id in the section 3.2.1 hierarchy, or to be an organisational recovery key with an independent compromise domain. A statement the compromised key could sign for itself is a tool for whoever stole it."
60+
},
61+
"sig": {
62+
"type": "object",
63+
"required": ["alg", "value"],
64+
"additionalProperties": false,
65+
"description": "Signature over the RFC 8785 canonical form of this object with `sig` absent.",
66+
"properties": {
67+
"alg": {
68+
"type": "string",
69+
"enum": ["ed25519", "ES256", "ES384"],
70+
"description": "Signature algorithm, matching the set spec section 3.2.1 allows."
71+
},
72+
"value": {
73+
"type": "string",
74+
"pattern": "^[A-Za-z0-9_-]+$",
75+
"description": "base64url, no padding."
76+
}
77+
}
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)