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 @@ -11,6 +11,8 @@ Format: [Semantic Versioning](https://semver.org/). Spec versions follow `MAJOR.

## [Unreleased]

- **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.

- 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
2 changes: 1 addition & 1 deletion docs/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ can share one. A value that needs to be larger is carried as a string. The same
| `appraisal` | object | **yes** | Verifier judgment |
| `transparency` | string | no | Registry or SCITT anchor for the record. Optional below Level 2, where an unanchored record has no receipt to name. Use `null`, never `""` |
| `cnf` | object | **yes** | Confirmation method: contains the `jwk` signing key |
| `signature` | string | **yes** | Base64url Ed25519 / ES256 / ES384 signature over the canonical record with only `signature` absent; `cnf` is included |
| `signature` | string | **yes** | Base64url Ed25519 / ES256 / ES384 signature over the canonical record with only `signature` absent; `cnf` is included. An 86-character value (a 64-byte Ed25519 or ES256 signature) MUST be canonically encoded per RFC 4648 section 3.5: it MUST end in `A`, `Q`, `g`, or `w` |

<a id="model"></a>

Expand Down
5 changes: 5 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ checking any archived record, including these.
verifies exactly as the pass does, unresolvable, and an issuer whose key the relying
party does not hold. Every digest recomputes; see that directory's README and
`docs/references-registry.md`.
- `signature-encoding/`: three signed Trust Records that separate a canonically-encoded
embedded signature from two non-canonical respellings of the same 64 bytes. Proposed,
not accepted normative text (#247); carries a `scan_published_signatures.py` tool
that reports how many published signatures are already canonical. See that
directory's README.

The schema sets `additionalProperties: false`, so examples must not carry
non-schema keys such as `_comment`. Keep descriptive notes in this file.
56 changes: 56 additions & 0 deletions examples/signature-encoding/01-canonical-signature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "canonical-signature",
"description": "An embedded signature in the canonical base64url spelling of its 64 bytes: the final character's four unused bits are zero, as RFC 4648 section 3.5 requires. Accepted today, and accepted under this proposal.",
"spec": "trace-v0.2 section 3.2.2, embedded signature bullet",
"profile": "trace.signature_encoding.proposal.v0",
"proposal": {
"issue": "agentrust-io/trace-spec#247",
"status": "under review, not accepted normative text"
},
"encoding": "canonical",
"decoded_signature_hex": "c86f6b95f7f6fca94cc278604b3e6efb20224e5ed75593aee635d70f7a653e6e5859a502716ab756603fc6dc1c460dc90832914582d91c7f93a528c3318aca08",
"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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "non-canonical-signature-respelling-low-bit",
"description": "The same 64 signature bytes as 01-canonical-signature.json, respelled with the final character's unused bits set to binary 0001 instead of 0000: the smallest change that makes the spelling non-canonical. RFC 4648 section 3.5 calls this spelling non-canonical. Accepted today; this proposal requires a verifier to reject it.",
"spec": "trace-v0.2 section 3.2.2, embedded signature bullet",
"profile": "trace.signature_encoding.proposal.v0",
"proposal": {
"issue": "agentrust-io/trace-spec#247",
"status": "under review, not accepted normative text"
},
"encoding": "non_canonical",
"canonical_counterpart": "01-canonical-signature.json",
"decoded_signature_hex": "c86f6b95f7f6fca94cc278604b3e6efb20224e5ed75593aee635d70f7a653e6e5859a502716ab756603fc6dc1c460dc90832914582d91c7f93a528c3318aca08",
"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-TpSjDMYrKCB"
},
"expected": {
"outcome": "rejected",
"failure": "signature_not_canonical"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "non-canonical-signature-respelling-all-bits",
"description": "The same 64 signature bytes as 01-canonical-signature.json, respelled with the final character's unused bits set to binary 1111 instead of 0000: every unused bit set, rather than 02's single bit. Pairs with 02 so the boundary is not covered by one specific bad string: a verifier must check the unused bits are zero, not special-case the one respelling that most naturally comes to mind. Accepted today; this proposal requires a verifier to reject it.",
"spec": "trace-v0.2 section 3.2.2, embedded signature bullet",
"profile": "trace.signature_encoding.proposal.v0",
"proposal": {
"issue": "agentrust-io/trace-spec#247",
"status": "under review, not accepted normative text"
},
"encoding": "non_canonical",
"canonical_counterpart": "01-canonical-signature.json",
"decoded_signature_hex": "c86f6b95f7f6fca94cc278604b3e6efb20224e5ed75593aee635d70f7a653e6e5859a502716ab756603fc6dc1c460dc90832914582d91c7f93a528c3318aca08",
"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-TpSjDMYrKCP"
},
"expected": {
"outcome": "rejected",
"failure": "signature_not_canonical"
}
}
Loading
Loading