-
Notifications
You must be signed in to change notification settings - Fork 30
Add optional event provenance and tamper-evidence guidance (#315) #320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MarkovianProtocol
wants to merge
3
commits into
cdevents:main
Choose a base branch
from
MarkovianProtocol:event-provenance-guidance
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
0f085aa
Add optional event provenance and tamper-evidence guidance
MarkovianProtocol 63a753a
Address review: restore wordlist ordering, vendor the worked example
MarkovianProtocol f1ef30d
Address second review round: CI fixes and CloudEvents note refresh
MarkovianProtocol File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| # Event provenance and tamper-evidence for CDEvents | ||
|
|
||
| Status: informational companion document. This guidance is optional and additive; it | ||
| does not change the core CDEvents specification or any schema. | ||
|
|
||
| ## Introduction | ||
|
|
||
| A CDEvent describes something that happened in a delivery pipeline: an artifact was | ||
| published, a build finished, a change was merged. Downstream systems increasingly | ||
| make decisions on these events (promotion gates, policy checks, audit trails), so two | ||
| separate questions arise: | ||
|
|
||
| 1. Authenticity and integrity. Who emitted this event, and has its payload been | ||
| altered in transit or at rest? | ||
| 2. Existence over time. Did this event actually exist by some point in time, and can | ||
| that be checked later without trusting the emitter's clock, or its continued | ||
| custody of a signing key? | ||
|
|
||
| These are different properties and they need different mechanisms. A signature answers | ||
| question 1. It does not answer question 2: a signature proves who and whether-tampered, | ||
| but not when the event existed, and a holder of the signing key can re-sign a | ||
| back-dated or quietly reissued event at any time. Question 2 needs a commitment to the | ||
| event that is fixed against an external timeline. | ||
|
|
||
| This document describes both, as optional, additive guidance. Nothing here changes the | ||
| core CDEvents schema. Part 1 covers signing a CDEvent with a | ||
| [DSSE](https://github.com/secure-systems-lab/dsse) envelope. Part 2 covers an optional | ||
| commitment to the event's canonical hash, anchored to an external timeline. Part 2 is | ||
| strictly opt-in and deliberately anchor-agnostic: the commitment is just the event | ||
| digest, and where it is anchored is an implementation choice. | ||
|
|
||
| ## Background: how a CDEvent is carried | ||
|
|
||
| Per the CDEvents [CloudEvents binding](https://github.com/cdevents/spec/blob/main/cloudevents-binding.md), | ||
| a CDEvent is transported as the `data` of a | ||
| [CloudEvent](https://github.com/cloudevents/spec) with `datacontenttype` | ||
| `application/json`, and the CloudEvents `id`, `source`, `type`, `subject` and `time` | ||
| attributes are mapped from the CDEvent's context. The guidance below can be applied at | ||
| either layer. Signing and hashing the CDEvent document (its `context` plus `subject`, | ||
| the object defined by the CDEvents schema) keeps provenance tied to the event itself and | ||
| independent of the transport, which is the approach used in the worked example. | ||
|
|
||
| ## Part 1: authenticity and integrity with DSSE | ||
|
|
||
| [DSSE](https://github.com/secure-systems-lab/dsse) (Dead Simple Signing Envelope) is the | ||
| signing envelope used by in-toto and SLSA attestations. It wraps an arbitrary payload | ||
| plus a payload-type string and one or more signatures, and it signs over a | ||
| Pre-Authentication Encoding (PAE) rather than the raw bytes, which removes | ||
| canonicalization ambiguity and cross-protocol confusion from the signature. | ||
|
|
||
| ### The envelope | ||
|
|
||
| The DSSE JSON envelope ([envelope.md](https://github.com/secure-systems-lab/dsse/blob/master/envelope.md)) | ||
| is: | ||
|
|
||
| ```json | ||
| { | ||
| "payload": "<Base64(SERIALIZED_BODY)>", | ||
| "payloadType": "<PAYLOAD_TYPE>", | ||
| "signatures": [{ "keyid": "<KEYID>", "sig": "<Base64(SIGNATURE)>" }] | ||
| } | ||
| ``` | ||
|
|
||
| - `payload` is the Base64 (RFC 4648) encoding of the serialized CDEvent. | ||
| - `payloadType` is an opaque, case-sensitive string identifying how to interpret the | ||
| payload. This guide uses `application/cdevents+json`. `payloadType` is authenticated, | ||
| so a signature over a CDEvent cannot be replayed as a signature over a different | ||
| document type. | ||
| - Each `signatures[].sig` is the Base64 signature. `keyid` is an optional, | ||
| unauthenticated hint for key selection. | ||
|
|
||
| ### What is signed: the PAE | ||
|
|
||
| DSSE signs the Pre-Authentication Encoding of the payload, defined in | ||
| [protocol.md](https://github.com/secure-systems-lab/dsse/blob/master/protocol.md): | ||
|
|
||
| ``` | ||
| PAE(type, body) = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body | ||
| SP = ASCII space (0x20) | ||
| LEN(s) = ASCII decimal byte length of s, no leading zeros | ||
| "DSSEv1" = ASCII bytes 44 53 53 45 76 31 | ||
| ``` | ||
|
|
||
| A verifier reconstructs `PAE(payloadType, Base64Decode(payload))` and checks the | ||
| signature against a trusted public key. Per DSSE guidance, the verified bytes are used | ||
| as-is; they are not re-parsed or re-canonicalized before use. | ||
|
|
||
| ### What DSSE proves, and what it does not | ||
|
|
||
| Proves: | ||
| - Authenticity. The event was signed by a holder of the private key for `keyid`. | ||
| - Integrity. Any change to the CDEvent payload, or to `payloadType`, invalidates the | ||
| signature. | ||
|
|
||
| Does not prove: | ||
| - When the event existed. A signature carries no trustworthy time. Any `timestamp` | ||
| inside the event is asserted by the emitter and can be set to any value. | ||
| - That the event was not reissued. A party holding the signing key can produce a fresh, | ||
| validly-signed event with different content at any later time. | ||
|
|
||
| Note on CloudEvents: CloudEvents has merged a DSSE-based verifiability extension | ||
| ([cloudevents/extensions/verifiability.md](https://github.com/cloudevents/spec/blob/main/cloudevents/extensions/verifiability.md), | ||
| merged June 2026), covering authenticity and integrity of individual events. When a | ||
| CDEvent travels as a CloudEvent, that extension is the recommended way to sign it — it | ||
| is DSSE in both cases, so Part 1 of this guidance and the extension align rather than | ||
| compete. The extension's stated non-goals (stream completeness, ordering, existence | ||
| over time) are what the Part 2 commitment adds; the two compose. | ||
|
|
||
| ## Part 2: existence over time with an optional hash commitment | ||
|
|
||
| To answer "did this event exist by time T", commit to the event and fix that commitment | ||
| against a timeline that the emitter does not control. This is independent of, and | ||
| complementary to, the DSSE signature. | ||
|
|
||
| ### The commitment | ||
|
|
||
| 1. Canonicalize the CDEvent with [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) | ||
| (JSON Canonicalization Scheme, JCS). JCS gives a single, deterministic byte | ||
| serialization for a JSON value, so any party recomputes the same bytes from the same | ||
| event. | ||
| 2. Hash the canonical bytes with SHA-256. This digest is the commitment. In the worked | ||
| example it is labeled `RFC8785-JCS+SHA-256`. | ||
| 3. Record the digest on an external timeline so that its existence-by-time-T can be | ||
| checked later. | ||
|
|
||
| Step 3 is where the anchor lives, and this guide is deliberately anchor-agnostic. A | ||
| digest can be fixed against a timeline in several ways, for example: | ||
|
|
||
| - an [RFC 3161](https://www.rfc-editor.org/rfc/rfc3161) trusted timestamp from a Time | ||
| Stamping Authority, | ||
| - an append-only transparency log (for example a Merkle-tree log in the style of | ||
| RFC 9162), | ||
| - inclusion in a public blockchain transaction. | ||
|
|
||
| Which anchor to use is an operational and threat-model choice. The only thing CDEvents | ||
| tooling needs to agree on is the commitment itself: the canonicalization | ||
| (RFC 8785) and the hash (SHA-256). Everything about the anchor is out of scope for this | ||
| document and MUST NOT be assumed by consumers. | ||
|
|
||
| ### What the commitment adds beyond DSSE | ||
|
|
||
| - Existence proof. Given the event, anyone can recompute the digest and check it against | ||
| the anchor's record, establishing that the event existed by the time the anchor was | ||
| written. | ||
| - Independence from key custody. The check does not require the emitter's signing key or | ||
| its clock. It survives key loss, key rotation, and emitter compromise after the anchor | ||
| time. | ||
| - Reissue detection. Because the digest is bound to a point on the timeline, a later | ||
| re-signed variant of the event produces a different digest with no earlier anchor. | ||
|
|
||
| The commitment does not prove authorship. Only the DSSE signature does. The two are | ||
| designed to be used together. | ||
|
|
||
| ## When to use which | ||
|
|
||
| | Requirement | DSSE signature (Part 1) | Hash commitment + anchor (Part 2) | | ||
| | --- | --- | --- | | ||
| | Prove who emitted the event | Yes | No | | ||
| | Detect payload tampering | Yes | Yes (digest mismatch) | | ||
| | Prove the event existed by time T | No | Yes | | ||
| | Verifiable without the emitter's key | No | Yes | | ||
| | Verifiable without trusting the emitter's clock | No | Yes | | ||
| | Detect a later re-signed / back-dated reissue | No | Yes | | ||
| | Extra infrastructure required | A key and key distribution | An external timeline / anchor | | ||
| | Status in this guidance | Recommended baseline | Optional, opt-in | | ||
|
|
||
| Baseline: sign CDEvents with DSSE. Add a Part 2 commitment when downstream decisions | ||
| need existence-over-time or must survive emitter key compromise, for example regulated | ||
| audit trails or long-lived provenance. | ||
|
|
||
| ## Worked example | ||
|
|
||
| A runnable end-to-end example accompanies this guide in | ||
| [`examples/event-provenance/`](./examples/event-provenance/). | ||
|
|
||
| The example uses real libraries rather than stubs: `rfc8785` for JCS canonicalization, | ||
| `cryptography` for Ed25519, and `jsonschema` to validate the sample event against the | ||
| published CDEvents artifact-published schema. It contains a valid | ||
| `dev.cdevents.artifact.published.0.3.0` event (CDEvents v0.5.1), its JCS canonical | ||
| serialization, the Part 2 commitment, a DSSE envelope over the canonical bytes, and a | ||
| tampered variant of each. Its verifier shows the genuine artifacts passing both checks | ||
| (the signature verifies, and SHA-256 of the signed payload equals the committed hash) | ||
| and the tampered artifacts failing both. | ||
|
|
||
| The example is illustrative. It uses one possible anchor mechanism; as stated in Part 2, | ||
| the choice of anchor is out of scope for this guidance. | ||
|
|
||
| ## References | ||
|
|
||
| - DSSE envelope format: secure-systems-lab/dsse, `envelope.md`. | ||
| https://github.com/secure-systems-lab/dsse/blob/master/envelope.md | ||
| - DSSE protocol and PAE: secure-systems-lab/dsse, `protocol.md`. | ||
| https://github.com/secure-systems-lab/dsse/blob/master/protocol.md | ||
| - RFC 8785, JSON Canonicalization Scheme (JCS). https://www.rfc-editor.org/rfc/rfc8785 | ||
| - RFC 4648, Base64 (used by DSSE `payload`/`sig`). https://www.rfc-editor.org/rfc/rfc4648 | ||
| - RFC 3161, Time-Stamp Protocol (one possible anchor for Part 2). | ||
| https://www.rfc-editor.org/rfc/rfc3161 | ||
| - RFC 9162, Certificate Transparency v2 (transparency-log style anchor for Part 2). | ||
| https://www.rfc-editor.org/rfc/rfc9162 | ||
| - CDEvents CloudEvents binding. | ||
| https://github.com/cdevents/spec/blob/main/cloudevents-binding.md | ||
| - CloudEvents specification. https://github.com/cloudevents/spec | ||
| - CloudEvents message-level signature discussion (open). | ||
| https://github.com/cloudevents/spec/issues/565 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # Event provenance worked example | ||
|
|
||
| A runnable example for the [event provenance guidance](../../event-provenance.md). | ||
| It contains a valid `dev.cdevents.artifact.published` event, its canonical | ||
| serialization, a DSSE envelope over the canonical bytes, the commitment to the | ||
| event digest, and a tampered variant of each. | ||
|
|
||
| Requires Python 3 with `rfc8785`, `cryptography`, and `jsonschema`: | ||
|
|
||
| ```shell | ||
| pip install rfc8785 cryptography jsonschema | ||
| ``` | ||
|
|
||
| Committed artifacts are already present. To regenerate and verify: | ||
|
|
||
| ```shell | ||
| python3 build_example.py | ||
| python3 verify_example.py | ||
| ``` | ||
|
|
||
| The verifier shows the genuine event passing both checks (the DSSE signature | ||
| verifies against the demo public key, and the SHA-256 digest of the canonical | ||
| payload equals the committed hash) and the tampered event failing both. The | ||
| demo keypair is generated by `build_example.py`; only the public key is | ||
| committed. `build_example.py` also validates the sample event against the | ||
| published CDEvents schema. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1