An attestation's issued_at, attester_id, key_id and algorithm are outside the signed preimage. A host that renders any of them beside a verified signature is presenting unauthenticated, rewritable text as though the signature covered it.
In plain words
A cryptographic attestation's signature only covers a small, fixed set of bytes — it does not cover who claims to have made the attestation (attester_id) or when they claim to have made it (issued_at). Anyone who touches the message in transit — a relaying host, a caching proxy, a compromised distribution step — can rewrite those two fields to say anything, and the signature will still verify as Valid. The spec never warns a reader of this, even though §6.5.2 sets the precedent of spelling out exactly this kind of boundary for a neighboring field.
§6.5.2 defines the frame commitment as exactly:
frame_commitment = SHA256(
"contextgraph/attest/1/frame"
‖ enc_str(provider_id) ‖ enc_str(frame.id)
‖ enc_opt(frame.content_digest)
‖ chain_head )
contextgraph-types/src/attest.rs:615 implements precisely that. Every other member of ProvenanceAttestation — signed_commitment aside — is metadata the signature does not reach.
What follows, stated plainly
issued_at is forgeable. It is the attestation's only temporal claim, and anyone in the path — a relaying host, a caching proxy, a registry, a compromised distribution step — can rewrite it to any value and the signature still verifies. has_well_formed_issued_at (attest.rs:187) checks its shape, which is the part an attacker has no reason to get wrong. A deployment that reasons about attestation freshness at all is reasoning about a number nothing protects.
attester_id is forgeable. AttestationState::Attested carries it forward verbatim (contextgraph-host/src/trust.rs), documented as "the attesting authority the attestation names — who is accountable for the claim, as distinct from the key that produced it". Accountability is exactly what an unsigned field cannot carry: it can be edited to name a different authority and the signature still verifies, because the verifier resolves the key by key_id and never reads attester_id.
key_id and algorithm are forgeable, but harmlessly — altering either makes verification fail or report UnknownAlgorithm, which is the safe direction. They are listed for completeness.
The spec says none of this. §6.5.4 is careful and thorough about verdict distinctions, F15 is explicit that a verifier must distinguish an attestation that binds content from one that does not — and then the same section is silent about the four fields a reader is most likely to assume are covered. §6.5.2 already sets the precedent for stating this kind of thing out loud: it devotes a paragraph to "What a signature binds when content_digest is absent", "because it is not what the presence of a signature suggests". The same sentence applies here and was not written.
Reproduce
let mut a = sign_frame_attestation(pid, &frame, &seed, "k1", "acme", "2026-08-29T00:00:00Z");
a.issued_at = "2099-01-01T00:00:00Z".into(); // any value
a.attester_id = "someone-else".into(); // any value
assert!(verify_frame_attestation(pid, &frame, &a, &pk).binds_content()); // still Valid
Why this needs a decision rather than a patch
There are two answers and they are not interchangeable.
-
Document it. Add a paragraph to §6.5.2 in the register of the content_digest one, and a rule — in the shape of F15 — that a verifier must not report unsigned attestation metadata as attested, plus guidance that a host surfacing issued_at or attester_id marks them as unverified. Cheap, non-breaking, honest.
-
Bind them. Put issued_at and attester_id into the preimage. This changes the commitment for every attestation ever produced, which under §13 U4 and §3.1 is a new major family — contextgraph/2. It cannot ride a 1.x minor.
Option 2 also raises a question option 1 does not: whether the protocol wants attestations to carry an authenticated timestamp at all, which is the door to expiry, replay windows, and revocation (see #136 on key lapse). That is a protocol-direction decision, not a bug fix.
Pillar
Stability. An attestation is evidence, and the boundary of what a signature covers is the whole of what evidence means. Leaving it unstated means two implementations will draw it differently, and the one that draws it generously will render forged metadata as signed.
Done looks like
- §6.5.2 or §6.5.4 states which members the commitment covers and which it does not, in a list, with the consequence spelled out for
issued_at and attester_id specifically.
- A requirement in the F15 family: a verifier MUST NOT present unsigned attestation metadata as covered by the signature, and a host SHOULD mark it as unverified where it surfaces it.
attest.rs module docs carry the same boundary, since they are what an implementer in another language reads.
- A decision recorded — an ADR — on whether an authenticated
issued_at is wanted in a future major family, so the question is closed rather than rediscovered.
- A regression test that pins today's behavior (
verify_frame_attestation still reports Valid/binds_content() for a tampered issued_at/attester_id) and is updated once the F15 MUST NOT rule ships, so the boundary decision has a fail→pass witness rather than only prose.
- Lands in
contextgraph-types (attest.rs) and contextgraph-host (trust.rs) without growing either into a new god file under this workspace's size conventions.
- Any new SPEC.md prose cites code by symbol (
sign_frame_attestation, AttestationState::Attested) rather than a pinned line number, matching this org's citation convention.
An attestation's
issued_at,attester_id,key_idandalgorithmare outside the signed preimage. A host that renders any of them beside a verified signature is presenting unauthenticated, rewritable text as though the signature covered it.In plain words
A cryptographic attestation's signature only covers a small, fixed set of bytes — it does not cover who claims to have made the attestation (
attester_id) or when they claim to have made it (issued_at). Anyone who touches the message in transit — a relaying host, a caching proxy, a compromised distribution step — can rewrite those two fields to say anything, and the signature will still verify asValid. The spec never warns a reader of this, even though §6.5.2 sets the precedent of spelling out exactly this kind of boundary for a neighboring field.§6.5.2 defines the frame commitment as exactly:
contextgraph-types/src/attest.rs:615implements precisely that. Every other member ofProvenanceAttestation—signed_commitmentaside — is metadata the signature does not reach.What follows, stated plainly
issued_atis forgeable. It is the attestation's only temporal claim, and anyone in the path — a relaying host, a caching proxy, a registry, a compromised distribution step — can rewrite it to any value and the signature still verifies.has_well_formed_issued_at(attest.rs:187) checks its shape, which is the part an attacker has no reason to get wrong. A deployment that reasons about attestation freshness at all is reasoning about a number nothing protects.attester_idis forgeable.AttestationState::Attestedcarries it forward verbatim (contextgraph-host/src/trust.rs), documented as "the attesting authority the attestation names — who is accountable for the claim, as distinct from the key that produced it". Accountability is exactly what an unsigned field cannot carry: it can be edited to name a different authority and the signature still verifies, because the verifier resolves the key bykey_idand never readsattester_id.key_idandalgorithmare forgeable, but harmlessly — altering either makes verification fail or reportUnknownAlgorithm, which is the safe direction. They are listed for completeness.The spec says none of this. §6.5.4 is careful and thorough about verdict distinctions, F15 is explicit that a verifier must distinguish an attestation that binds content from one that does not — and then the same section is silent about the four fields a reader is most likely to assume are covered. §6.5.2 already sets the precedent for stating this kind of thing out loud: it devotes a paragraph to "What a signature binds when
content_digestis absent", "because it is not what the presence of a signature suggests". The same sentence applies here and was not written.Reproduce
Why this needs a decision rather than a patch
There are two answers and they are not interchangeable.
Document it. Add a paragraph to §6.5.2 in the register of the
content_digestone, and a rule — in the shape of F15 — that a verifier must not report unsigned attestation metadata as attested, plus guidance that a host surfacingissued_atorattester_idmarks them as unverified. Cheap, non-breaking, honest.Bind them. Put
issued_atandattester_idinto the preimage. This changes the commitment for every attestation ever produced, which under §13 U4 and §3.1 is a new major family —contextgraph/2. It cannot ride a1.xminor.Option 2 also raises a question option 1 does not: whether the protocol wants attestations to carry an authenticated timestamp at all, which is the door to expiry, replay windows, and revocation (see #136 on key lapse). That is a protocol-direction decision, not a bug fix.
Pillar
Stability. An attestation is evidence, and the boundary of what a signature covers is the whole of what evidence means. Leaving it unstated means two implementations will draw it differently, and the one that draws it generously will render forged metadata as signed.
Done looks like
issued_atandattester_idspecifically.attest.rsmodule docs carry the same boundary, since they are what an implementer in another language reads.issued_atis wanted in a future major family, so the question is closed rather than rediscovered.verify_frame_attestationstill reportsValid/binds_content()for a tamperedissued_at/attester_id) and is updated once the F15 MUST NOT rule ships, so the boundary decision has a fail→pass witness rather than only prose.contextgraph-types(attest.rs) andcontextgraph-host(trust.rs) without growing either into a new god file under this workspace's size conventions.sign_frame_attestation,AttestationState::Attested) rather than a pinned line number, matching this org's citation convention.