Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ Format: [Semantic Versioning](https://semver.org/). Spec versions follow `MAJOR.

- **Proposal: the embedded `signature` value must be canonically encoded (#247). Incompatible tightening of the draft's acceptance rules.** Section 3.2.2 already names the embedded `signature` field's encoding as base64url, no padding; it does not say whether a different spelling of the same bytes is the same record. A 64-byte signature (Ed25519 or ES256) spends 86 base64url characters on 512 bits, leaving 4 unused bits in the final character; RFC 4648 section 3.5 requires those bits to be zero. The added sentence requires the canonical spelling and requires a verifier to reject a record whose signature is not canonically encoded. The schema `pattern` is tightened so an 86-character value's final character must be `A`, `Q`, `g`, or `w`; `models.py` (through the shared pattern helper of #412), the packaged schema copy, and the v0.3 draft (regenerated from canonical) track it. **Newly rejected:** an 86-character `signature` ending in any of the other 60 base64url characters, which the previous pattern `^[A-Za-z0-9_-]+$` accepted; each valid signature had 15 such respellings. The reference library verified them through 0.10.0, because its decoder discarded the unused bits, and has refused them at decode time since 0.11.0 (#418); this change makes the schema, the models and the normative text give the same answer, so a consumer that validates a record without decoding its signature refuses it too. Values of any other length are unaffected; a 96-byte ES384 signature (128 characters) has no unused bits. **Existing records:** a record whose `signature` is non-canonical is rejected. The signature bytes are unchanged, so zeroing the final character's unused bits makes the record verify again without re-signing; the respelled record is nevertheless a different record for section 3.1.3, whose `parent_record_hash` covers the parent's `signature` member, so any child that names the old spelling must be re-issued. `examples/signature-encoding/` adds a canonical vector, reusing an already-published record and signature, and two non-canonical respellings of the same 64 bytes with different unused-bit patterns, plus a re-runnable `scan_published_signatures.py`. **Scan, limited to the revisions examined:** this repository's `examples/` at `63f4d15`, `agentrust-io/trace-tests` at `da4369b` and `agentrust-io/trace-registry` at `e26b85a` hold 232 signatures, all canonical; the 2 non-canonical values the scan also reports are this change's own deliberately non-canonical vectors. The scan says nothing about records outside those revisions. The `cnf.jwk.x` and trusted-key cases raise the same question for the specification text and are not part of this change; the library's decoder has treated them alike since #418. Requested by @imran-siddique on #247; proposed by @chernistry.

- **Proposal: integer-typed members are decided by value, not by how they are written (#247).** JSON has one number type, and RFC 8785 writes a number by its value, so `iat` written `1785000000`, `1785000000.0` or `1.785e9` has one pre-image and one signature. The schemas already decided `integer` by value, as JSON Schema 2020-12 defines it. `verify_record` did not: its freshness check tested for a Python `int`, `json.loads` returns a `float` for the other two spellings, and a correctly signed record that had just passed the schema was rejected for having "no valid integer 'iat'". Section 3.2.2 gains a paragraph, "What counts as an integer", stating the rule and that the value is the IEEE 754 double RFC 8785 serializes. In the reference, the freshness check and the bridge's `authorized_at` and `expires_at` checks and the `observed_at` checks of the successor envelope and of the successor artifact accept a whole number however it is written and name the rule a value fails: not an integer value, or outside the safe-integer range. `_canonical_bytes`, and through it signing, every digest and both adapters' transcript hashes, refuses a whole number past the range written as a float, as it already refused one written as an integer. The model reads `1785000000.0` as the integer it is. Two inputs that were accepted are now refused by the library: a numeric string such as `"1785000000"`, which the model read as an integer and the schema never accepted, and a whole float past the range in an object the library signs or digests, such as a nanosecond timestamp that went through a double. Neither occurs in a Trust Record that passes the schema. The second changes a verdict where no schema stands in front of the canonicalizer, which Scope below spells out. `examples/number-spelling/` adds seven vectors, a JavaScript test that runs on Node.js with no dependencies, and a scan of published records: of 1,341 numbers in this repository's examples, spec, docs and schemas, in `agentrust-io/trace-tests` at `da4369b` and in `agentrust-io/trace-registry` at `e26b85a`, 549 are integer-typed and none is written with a fraction or an exponent; the one other number written that way, `1e400` in a `trace-tests` vector built to be refused, has no finite value and no canonical form before or after. No published verdict changes. **Scope.** The paragraph governs every object the range rule of section 3.2.2 covers. For the range, that includes the MCP Server Provenance Record (`spec/server-provenance-v1.md` and `spec/server-provenance-v2.md`), which is signed under section 3.2, and the transcript behind section 3.1.4's `transcript_digest`. `provenance.sign_record` and `provenance.verify_record` canonicalize through `_canonical_bytes`, so a provenance record of either format that carries `1e21`, `9007199254740992.0`, `-1e21` or `-9007199254740992.0` in a member its verifier does not type, which signed and verified through 0.11.0, is refused by both. The paragraph retypes no provenance member: `provenance.py` keeps its type-based checks on `issued_at` and `tool_count`, and whether those are decided by value is a separate question, not settled here. Registry-anchor encoding is not touched. **Python API compatibility (breaking for callers, not for records that pass the schema).** (1) `TrustRecord` and the other models no longer read a numeric string as a number: `"iat": "1785000000"` raised nothing before and raises a validation error now (`tests/test_integer_by_value.py`, the `got a str` and `is a str, not a JSON number` cases). The schemas never accepted it, so no record that passed `validate` is affected. (2) `_canonical_bytes` refuses a whole float past the safe-integer range, such as `1e21` or a nanosecond timestamp that went through a double; before, the float was written as it was, giving a pre-image no conformant verifier could re-derive. Each caller reports the refusal in its own error type: `sign.sign_record`, `revocation.bundle_digest` and both adapters' transcript hashes raise `rfc8785.IntegerDomainError`, a `ValueError`; `intent_bridge.digest_jcs`, `sign_bridge` and `verify_bridge` raise `IntentBridgeError`; `provenance.sign_record` and `provenance.verify_record` raise `ProvenanceError`, for both provenance formats (`test_every_digest_refuses_a_whole_number_past_the_range_written_as_a_float`, `test_each_signing_function_refuses_a_whole_float_past_the_range_in_its_own_error_type`, `test_a_provenance_record_refuses_a_whole_float_past_the_range_when_signing_and_verifying`). (3) `verify_record`'s freshness check and the bridge's time checks accept a whole float where they demanded a Python `int`; a call that was rejected with `no valid integer 'iat'` now verifies, and the rejection message for a non-integer names the value that failed. Nothing else in the public API changes signature or return type. Requested by @imran-siddique on #247; proposed by @chernistry.

- Propose explicit verifier profile declarations and verifier-result field requirements for obligations 2 and 3 of #116, carried by Imran Siddique following the discussion with @lywinged. Obligations 1 and 4 remain deferred; the v0.1 cutover is unchanged.
- **Breaking, proposed: `silent` mode allows a policy-denied action (#143, following #28).** Section 4.3 now says a deny MUST NOT block in `silent` mode and the audit chain MUST record every would-have-denied decision. v0.9.0 and v0.10.0 described `silent` as evaluated and enforced under the same `eat_profile`, so a producer that followed that text and blocks on deny no longer conforms, and a `silent` record does not say which meaning it was written under. Classified breaking under GOVERNANCE.md, so adoption needs the 30-day comment period and Project Lead sign-off. Section 4.3 also orders the modes: `advisory` is not weaker than `silent`. Enforce remains the default; schema constraints and runtime behavior are unchanged. Classification raised by @lywinged.

Expand Down
5 changes: 5 additions & 0 deletions docs/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ unambiguously, since RFC 8785 serializes numbers through an IEEE 754 double and
can share one. A value that needs to be larger is carried as a string. The same bound applies to members a
`cnf.jwk` carries that this schema does not name.

Whether a number is an integer is decided by its value, not by how it is written (spec section 3.2.2, "What
counts as an integer"). `1785000000.0` and `1.785e9` are the integer 1785000000; `1785000000.5` is not an
integer. JSON Schema defines `integer` the same way, so validating against this schema already gives that
answer.

<a id="top-level-fields"></a>

## Top-level fields {#trace-fields}
Expand Down
6 changes: 6 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ checking any archived record, including these.
§3.2.2 requires and names as insufficient. Each file is a test-vector envelope;
the Trust Record is under the `record` key and validates against the schema.
See that directory's README.
- `number-spelling/`: seven signed Trust Records for a rule proposed on #247 and not yet
accepted: an integer-typed member is decided by its value, not by how it is written.
`iat` written `1785000000`, `1785000000.0` and `1.785e9` carries one signature and
verifies; a fractional value and a value past the safe-integer range are each rejected
in two spellings. Each file is a test-vector envelope with the record under `record`.
See that directory's README, which also counts the published records the rule changes.
- `condition-appraisal/`: five signed Trust Records, each citing an independent check's
finding through a `references` entry with `rel: "condition-appraisal"`, against an
appraisal store committed beside them: verified, altered after issue, a fail that
Expand Down
61 changes: 61 additions & 0 deletions examples/number-spelling/01-integer-spelling-verified.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "integer-spelling-verified",
"description": "The record and signature published as examples/verifier-compatibility/01-known-version-verified.json, with iat in the plain integer spelling. The baseline the next two vectors re-spell.",
"spec": "trace-v0.2 section 3.2.2, What counts as an integer",
"profile": "trace.number_spelling.proposal.v0",
"proposal": {
"issue": "agentrust-io/trace-spec#247",
"status": "under review, not accepted normative text"
},
"member": "iat",
"spelling": "1785000000",
"verifier": {
"verification_time": 1785000100,
"max_age_seconds": 86400,
"max_future_skew_seconds": 300
},
"trusted_key": {
"kty": "OKP",
"crv": "Ed25519",
"x": "S9f_IqfsGG7jIzI7brZ7VVXCyF-_1jj_Guc1ynPKpuI"
},
"record": {
"eat_profile": "tag:agentrust-io.com,2026:trace-v0.2",
"iat": 1785000000,
"subject": "spiffe://factory.example/agent/payments/prod",
"model": {
"provider": "example-provider",
"model_id": "example-model-1"
},
"runtime": {
"platform": "software-only",
"measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
},
"policy": {
"bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"enforcement_mode": "enforce"
},
"data_class": "confidential",
"build_provenance": {
"slsa_level": 0,
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
},
"appraisal": {
"status": "affirming",
"verifier": "https://verifier.example/v1"
},
"transparency": "https://rekor.example/api/v1/log/entries/0",
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "S9f_IqfsGG7jIzI7brZ7VVXCyF-_1jj_Guc1ynPKpuI"
}
},
"signature": "yG9rlff2_KlMwnhgSz5u-yAiTl7XVZOu5jXXD3plPm5YWaUCcWq3VmA_xtwcRg3JCDKRRYLZHH-TpSjDMYrKCA"
},
"expected": {
"outcome": "verified",
"failure": null
}
}
62 changes: 62 additions & 0 deletions examples/number-spelling/02-fraction-spelling-verified.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"name": "fraction-spelling-verified",
"description": "The same record with iat written 1785000000.0. The value is the integer 1785000000: JSON has one number type, and RFC 8785 writes this value as 1785000000, so the pre-image and the signature are 01's, byte for byte. Python's json.loads returns a float here and an int for 01; JSON.parse returns one number for both, so a JavaScript verifier cannot tell this record from 01 at all.",
"spec": "trace-v0.2 section 3.2.2, What counts as an integer",
"profile": "trace.number_spelling.proposal.v0",
"proposal": {
"issue": "agentrust-io/trace-spec#247",
"status": "under review, not accepted normative text"
},
"member": "iat",
"spelling": "1785000000.0",
"same_signature_as": "01-integer-spelling-verified.json",
"verifier": {
"verification_time": 1785000100,
"max_age_seconds": 86400,
"max_future_skew_seconds": 300
},
"trusted_key": {
"kty": "OKP",
"crv": "Ed25519",
"x": "S9f_IqfsGG7jIzI7brZ7VVXCyF-_1jj_Guc1ynPKpuI"
},
"record": {
"eat_profile": "tag:agentrust-io.com,2026:trace-v0.2",
"iat": 1785000000.0,
"subject": "spiffe://factory.example/agent/payments/prod",
"model": {
"provider": "example-provider",
"model_id": "example-model-1"
},
"runtime": {
"platform": "software-only",
"measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
},
"policy": {
"bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"enforcement_mode": "enforce"
},
"data_class": "confidential",
"build_provenance": {
"slsa_level": 0,
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
},
"appraisal": {
"status": "affirming",
"verifier": "https://verifier.example/v1"
},
"transparency": "https://rekor.example/api/v1/log/entries/0",
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "S9f_IqfsGG7jIzI7brZ7VVXCyF-_1jj_Guc1ynPKpuI"
}
},
"signature": "yG9rlff2_KlMwnhgSz5u-yAiTl7XVZOu5jXXD3plPm5YWaUCcWq3VmA_xtwcRg3JCDKRRYLZHH-TpSjDMYrKCA"
},
"expected": {
"outcome": "verified",
"failure": null
}
}
62 changes: 62 additions & 0 deletions examples/number-spelling/03-exponent-spelling-verified.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"name": "exponent-spelling-verified",
"description": "The same record with iat written 1.785e9, the exponent spelling of the same value, with 01's pre-image and 01's signature. Two accepting re-spellings rather than one: an implementation that refuses an exponent, or that accepts 02 by stripping a trailing .0 from the text, verifies 02 and rejects this one.",
"spec": "trace-v0.2 section 3.2.2, What counts as an integer",
"profile": "trace.number_spelling.proposal.v0",
"proposal": {
"issue": "agentrust-io/trace-spec#247",
"status": "under review, not accepted normative text"
},
"member": "iat",
"spelling": "1.785e9",
"same_signature_as": "01-integer-spelling-verified.json",
"verifier": {
"verification_time": 1785000100,
"max_age_seconds": 86400,
"max_future_skew_seconds": 300
},
"trusted_key": {
"kty": "OKP",
"crv": "Ed25519",
"x": "S9f_IqfsGG7jIzI7brZ7VVXCyF-_1jj_Guc1ynPKpuI"
},
"record": {
"eat_profile": "tag:agentrust-io.com,2026:trace-v0.2",
"iat": 1.785e9,
"subject": "spiffe://factory.example/agent/payments/prod",
"model": {
"provider": "example-provider",
"model_id": "example-model-1"
},
"runtime": {
"platform": "software-only",
"measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
},
"policy": {
"bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"enforcement_mode": "enforce"
},
"data_class": "confidential",
"build_provenance": {
"slsa_level": 0,
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
},
"appraisal": {
"status": "affirming",
"verifier": "https://verifier.example/v1"
},
"transparency": "https://rekor.example/api/v1/log/entries/0",
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "S9f_IqfsGG7jIzI7brZ7VVXCyF-_1jj_Guc1ynPKpuI"
}
},
"signature": "yG9rlff2_KlMwnhgSz5u-yAiTl7XVZOu5jXXD3plPm5YWaUCcWq3VmA_xtwcRg3JCDKRRYLZHH-TpSjDMYrKCA"
},
"expected": {
"outcome": "verified",
"failure": null
}
}
61 changes: 61 additions & 0 deletions examples/number-spelling/04-fractional-value-rejected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "fractional-value-rejected",
"description": "iat written 1785000000.5, a number that is not a whole number and so is not an integer. The signature is valid over this record's own RFC 8785 form, so the only reason to reject it is the rule under test.",
"spec": "trace-v0.2 section 3.2.2, What counts as an integer",
"profile": "trace.number_spelling.proposal.v0",
"proposal": {
"issue": "agentrust-io/trace-spec#247",
"status": "under review, not accepted normative text"
},
"member": "iat",
"spelling": "1785000000.5",
"verifier": {
"verification_time": 1785000100,
"max_age_seconds": 86400,
"max_future_skew_seconds": 300
},
"trusted_key": {
"kty": "OKP",
"crv": "Ed25519",
"x": "S9f_IqfsGG7jIzI7brZ7VVXCyF-_1jj_Guc1ynPKpuI"
},
"record": {
"eat_profile": "tag:agentrust-io.com,2026:trace-v0.2",
"iat": 1785000000.5,
"subject": "spiffe://factory.example/agent/payments/prod",
"model": {
"provider": "example-provider",
"model_id": "example-model-1"
},
"runtime": {
"platform": "software-only",
"measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
},
"policy": {
"bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"enforcement_mode": "enforce"
},
"data_class": "confidential",
"build_provenance": {
"slsa_level": 0,
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
},
"appraisal": {
"status": "affirming",
"verifier": "https://verifier.example/v1"
},
"transparency": "https://rekor.example/api/v1/log/entries/0",
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "S9f_IqfsGG7jIzI7brZ7VVXCyF-_1jj_Guc1ynPKpuI"
}
},
"signature": "pauSvCiX3XmhkLW1Pl0zMHkKbwofRBAwJl3KKuq3cSyVo3PiXwWTML93RreNEf7CsxUA3oBUBb-QsVnB0P9TCw"
},
"expected": {
"outcome": "rejected",
"failure": "not_an_integer_value"
}
}
Loading
Loading