A Valid Signature Doesn't Tell You Who Signed It
Last time we said a 200 OK is not an audit trail, and that CyclesEvidence closes the gap by turning eligible budget decisions into signed, content-addressed receipts. We also left an honest boundary open at the end: the signature proves the bytes, but signer authority — proving the signing key genuinely belongs to that server — was still to come.
This is that part. Because the moment you hand someone a signed receipt, they ask the second question, and it's sharper than the first: signed by whom — and how do you know?
The forgery a valid signature doesn't catch
A signature check is mechanical: it takes some bytes, a signature, and a public key, and tells you the three are consistent. That's real and useful — change one byte of the receipt and the check fails.
But look at what it doesn't say. If the public key it checks against is simply the one carried inside the receipt, then a forger writes their own receipt, embeds their own key, and signs it with the matching private key. Every field is internally consistent. The signature verifies. But all it proves is that whoever controlled the embedded key signed those bytes — it does not prove the claimed server ever authorized that key.
"The signature is valid" answers do these bytes match this key. It does not answer is this key allowed to speak for that server. Those are two different questions, and only the second one is trust.
And the second one is the one your auditor, regulator, counterparty, or incident reviewer actually cares about. Without signer authority, a "signed" receipt is awkward to hand to any of them: the verifier still has to either trust whatever key the receipt carried or have manually pinned the signer in advance. That's not evidence that stands on its own — it's evidence with a footnote.
Validity and authority are different axes
- Signature validity — do the bytes verify against the key named in the envelope? Self-contained, cheap, no network. This is what shipped first.
- Signer authority — is that key actually the legitimate signer for the server the envelope claims to come from? This needs an anchor outside the receipt.
Validity without authority is still worth something — we call it binding-only: the signature is cryptographically sound, and you may have pinned ahead of time which signer you expect, so a match means "the key I already decided to trust signed this." That's shippable and honest for a known counterparty. But notice the trust came from you pinning a key, not from the system proving the key is the server's. Authority is what removes that homework — and it's what an auditor or an untrusting third party, who never pinned anything, actually needs.
Resolving the signer
Closing the gap means anchoring the key to the server's own published identity rather than to the receipt. The server publishes its verification keys — public keys only, never the private signing key — at a well-known location derived from its identity. A consumer takes the signer reference on the envelope, resolves it to that published key set, and confirms the signing key is actually in it.
Now the trust anchor is the server's identity, not a key the receipt handed you. A forger can still embed their own key and self-sign — but it won't be in the real server's published set, so it resolves to not authorized, not authentic. (The exact identifier and key-set format are being nailed down with our first cross-system consumer; the mechanics live in the envelope reference and the protocol issue it tracks.)
Status. Signed, content-addressed evidence works today — and a server can publish its verification key set today. Full signer resolution (a consumer resolving the envelope's signer to that set, end-to-end) is being finalized with the first cross-system consumer. Until it round-trips, pin the expected signer and read the result as
binding_only— which is exactly enough for a known counterparty. The boundary is deliberate, not a missing feature.
The rotation trap — the part that's easy to get catastrophically wrong
Keys rotate. They should: a signing key has a lifetime, and rotating it is hygiene. Here's where a naive resolver quietly destroys your audit trail.
The tempting implementation fetches the key set and uses the current key. It passes every test you write today, because today's receipts were signed by today's key. Then you rotate — and every receipt signed before the rotation stops verifying, all at once, because it's being checked against a key that didn't exist when it was signed. For a live system that's an outage. For an audit trail meant to last years, it's the whole asset evaporating on a routine key change.
So the design has the published key set keep retired keys, each stamped with the window of time it was valid, and the verifier selects the key whose window covers the receipt's issuance time — never "the latest." A receipt from eight months and two rotations ago still verifies, against the key that was genuinely valid when it was signed. This is the unglamorous mechanism that long-lived AI-governance records need: regimes like the EU AI Act expect automated decision records to be retained and reviewable long after the fact — and "long after the fact" routinely outlives a signing key. Evidence has to survive the rotation, not silently rot on the next key change. (Today a server publishes a single active key; the retired-key history and validity windows are the next step on the publication side — but the verifier rule is built for them from the start, because retrofitting "pick the key for the signing time" after you've rotated is exactly the outage you're trying to avoid.)
Honesty is a disposition, not a boolean
Once resolution involves fetching something, a new failure mode appears: you couldn't reach the key directory. The dangerous move is to fold that into the signature result. "I couldn't fetch the keys" is not "this is forged" — collapsing them turns a network blip into a false fraud alarm, or, worse, lets a real tamper hide behind an ambiguous "couldn't check."
So the outcome of verification isn't valid: true/false. It's a small set of distinct dispositions (the protocol's identifier is in parentheses):
- authentic (
authentic) — signature valid and the key resolved to the server's published set for the time it was signed. Both axes pass. - binding-only (
binding_only) — signature valid, but authority wasn't established (no resolution, or a pinned-issuer posture). Honest about what it does and doesn't prove. - authority-not-established (
signer_authority_failed) — the key set resolved fine, but the signing key isn't in it (or isn't valid for this server at that time). This is exactly where the embedded-key forgery lands — and it is neither a network failure nor a proof of tamper. - could-not-resolve (
signer_resolution_failed) — the key directory was unreachable or unparseable. Establishes nothing about the bytes. Must never read as "invalid." - invalid (
signature_invalid) — the bytes don't verify against the resolved key. Failed cryptographic integrity: it may be tampering, but it can equally be corruption, an encoding or canonicalization mismatch, or a verifier/envelope version skew. Serious, but not automatically "someone forged this."
Three of those five are "valid signature, but not authentic" for three genuinely different reasons — and the failures (couldn't-resolve, authority-not-established, invalid) stay rigidly separate: a network blip, an unauthorized key, and a failed-integrity check are not the same event. Collapsing them is how verifiers lie. That's why the result is a taxonomy, not a checkbox.
The honest boundary (again, on purpose)
Two things this still isn't, because over-claiming an audit feature is its own risk:
- It's about who signed, not whether the decision was right. Authority resolution tells you the receipt genuinely came from that server. It says nothing about whether the budget call was correct — enforcement is still the reserve/commit ledger.
- It's opt-in and still settling. A server can publish its key set today; resolving it end-to-end is landing with our first cross-system consumer, and the identifier shape is being finalized with them. Until that round-trips, pin the expected signer for issuer trust — the binding-only path is exactly enough for the pinned case right now.
Close the loop
CyclesEvidence is built to keep these two claims separate and explicit: binding_only when the bytes verify, authentic when the signer also resolves to an authorized server key, and distinct failure states when authority can't be established — never a single green checkmark papering over the difference.
If your agents touch money, tools, or anything with a blast radius, "it's signed" is not the end of the auditor's question. The real question is: signed by whom, under what authority, and will it still verify after the keys have rotated? A signature gives you byte integrity. Signer authority gives you trust — and the job is to keep that trust separate, explicit, and durable over time.