|
| 1 | +# Event provenance and tamper-evidence for CDEvents |
| 2 | + |
| 3 | +Status: informational companion document. This guidance is optional and additive; it |
| 4 | +does not change the core CDEvents specification or any schema. |
| 5 | + |
| 6 | +## Introduction |
| 7 | + |
| 8 | +A CDEvent describes something that happened in a delivery pipeline: an artifact was |
| 9 | +published, a build finished, a change was merged. Downstream systems increasingly |
| 10 | +make decisions on these events (promotion gates, policy checks, audit trails), so two |
| 11 | +separate questions arise: |
| 12 | + |
| 13 | +1. Authenticity and integrity. Who emitted this event, and has its payload been |
| 14 | + altered in transit or at rest? |
| 15 | +2. Existence over time. Did this event actually exist by some point in time, and can |
| 16 | + that be checked later without trusting the emitter's clock, or its continued |
| 17 | + custody of a signing key? |
| 18 | + |
| 19 | +These are different properties and they need different mechanisms. A signature answers |
| 20 | +question 1. It does not answer question 2: a signature proves who and whether-tampered, |
| 21 | +but not when the event existed, and a holder of the signing key can re-sign a |
| 22 | +back-dated or quietly reissued event at any time. Question 2 needs a commitment to the |
| 23 | +event that is fixed against an external timeline. |
| 24 | + |
| 25 | +This document describes both, as optional, additive guidance. Nothing here changes the |
| 26 | +core CDEvents schema. Part 1 covers signing a CDEvent with a |
| 27 | +[DSSE](https://github.com/secure-systems-lab/dsse) envelope. Part 2 covers an optional |
| 28 | +commitment to the event's canonical hash, anchored to an external timeline. Part 2 is |
| 29 | +strictly opt-in and deliberately anchor-agnostic: the commitment is just the event |
| 30 | +digest, and where it is anchored is an implementation choice. |
| 31 | + |
| 32 | +## Background: how a CDEvent is carried |
| 33 | + |
| 34 | +Per the CDEvents [CloudEvents binding](https://github.com/cdevents/spec/blob/main/cloudevents-binding.md), |
| 35 | +a CDEvent is transported as the `data` of a |
| 36 | +[CloudEvent](https://github.com/cloudevents/spec) with `datacontenttype` |
| 37 | +`application/json`, and the CloudEvents `id`, `source`, `type`, `subject` and `time` |
| 38 | +attributes are mapped from the CDEvent's context. The guidance below can be applied at |
| 39 | +either layer. Signing and hashing the CDEvent document (its `context` plus `subject`, |
| 40 | +the object defined by the CDEvents schema) keeps provenance tied to the event itself and |
| 41 | +independent of the transport, which is the approach used in the worked example. |
| 42 | + |
| 43 | +## Part 1: authenticity and integrity with DSSE |
| 44 | + |
| 45 | +[DSSE](https://github.com/secure-systems-lab/dsse) (Dead Simple Signing Envelope) is the |
| 46 | +signing envelope used by in-toto and SLSA attestations. It wraps an arbitrary payload |
| 47 | +plus a payload-type string and one or more signatures, and it signs over a |
| 48 | +Pre-Authentication Encoding (PAE) rather than the raw bytes, which removes |
| 49 | +canonicalization ambiguity and cross-protocol confusion from the signature. |
| 50 | + |
| 51 | +### The envelope |
| 52 | + |
| 53 | +The DSSE JSON envelope ([envelope.md](https://github.com/secure-systems-lab/dsse/blob/master/envelope.md)) |
| 54 | +is: |
| 55 | + |
| 56 | +```json |
| 57 | +{ |
| 58 | + "payload": "<Base64(SERIALIZED_BODY)>", |
| 59 | + "payloadType": "<PAYLOAD_TYPE>", |
| 60 | + "signatures": [{ "keyid": "<KEYID>", "sig": "<Base64(SIGNATURE)>" }] |
| 61 | +} |
| 62 | +``` |
| 63 | + |
| 64 | +- `payload` is the Base64 (RFC 4648) encoding of the serialized CDEvent. |
| 65 | +- `payloadType` is an opaque, case-sensitive string identifying how to interpret the |
| 66 | + payload. This guide uses `application/cdevents+json`. `payloadType` is authenticated, |
| 67 | + so a signature over a CDEvent cannot be replayed as a signature over a different |
| 68 | + document type. |
| 69 | +- Each `signatures[].sig` is the Base64 signature. `keyid` is an optional, |
| 70 | + unauthenticated hint for key selection. |
| 71 | + |
| 72 | +### What is signed: the PAE |
| 73 | + |
| 74 | +DSSE signs the Pre-Authentication Encoding of the payload, defined in |
| 75 | +[protocol.md](https://github.com/secure-systems-lab/dsse/blob/master/protocol.md): |
| 76 | + |
| 77 | +``` |
| 78 | +PAE(type, body) = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body |
| 79 | +SP = ASCII space (0x20) |
| 80 | +LEN(s) = ASCII decimal byte length of s, no leading zeros |
| 81 | +"DSSEv1" = ASCII bytes 44 53 53 45 76 31 |
| 82 | +``` |
| 83 | + |
| 84 | +A verifier reconstructs `PAE(payloadType, Base64Decode(payload))` and checks the |
| 85 | +signature against a trusted public key. Per DSSE guidance, the verified bytes are used |
| 86 | +as-is; they are not re-parsed or re-canonicalized before use. |
| 87 | + |
| 88 | +### What DSSE proves, and what it does not |
| 89 | + |
| 90 | +Proves: |
| 91 | +- Authenticity. The event was signed by a holder of the private key for `keyid`. |
| 92 | +- Integrity. Any change to the CDEvent payload, or to `payloadType`, invalidates the |
| 93 | + signature. |
| 94 | + |
| 95 | +Does not prove: |
| 96 | +- When the event existed. A signature carries no trustworthy time. Any `timestamp` |
| 97 | + inside the event is asserted by the emitter and can be set to any value. |
| 98 | +- That the event was not reissued. A party holding the signing key can produce a fresh, |
| 99 | + validly-signed event with different content at any later time. |
| 100 | + |
| 101 | +Note on CloudEvents: a message-level signature profile for CloudEvents itself is an open |
| 102 | +discussion in the CloudEvents project |
| 103 | +([cloudevents/spec#565](https://github.com/cloudevents/spec/issues/565)) rather than a |
| 104 | +finished standard. Applying DSSE to the CDEvent document, as described here, does not |
| 105 | +depend on that work and is compatible with signing at either the CDEvent or the |
| 106 | +CloudEvents layer. |
| 107 | + |
| 108 | +## Part 2: existence over time with an optional hash commitment |
| 109 | + |
| 110 | +To answer "did this event exist by time T", commit to the event and fix that commitment |
| 111 | +against a timeline that the emitter does not control. This is independent of, and |
| 112 | +complementary to, the DSSE signature. |
| 113 | + |
| 114 | +### The commitment |
| 115 | + |
| 116 | +1. Canonicalize the CDEvent with [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) |
| 117 | + (JSON Canonicalization Scheme, JCS). JCS gives a single, deterministic byte |
| 118 | + serialization for a JSON value, so any party recomputes the same bytes from the same |
| 119 | + event. |
| 120 | +2. Hash the canonical bytes with SHA-256. This digest is the commitment. In the worked |
| 121 | + example it is labeled `RFC8785-JCS+SHA-256`. |
| 122 | +3. Record the digest on an external timeline so that its existence-by-time-T can be |
| 123 | + checked later. |
| 124 | + |
| 125 | +Step 3 is where the anchor lives, and this guide is deliberately anchor-agnostic. A |
| 126 | +digest can be fixed against a timeline in several ways, for example: |
| 127 | + |
| 128 | +- an [RFC 3161](https://www.rfc-editor.org/rfc/rfc3161) trusted timestamp from a Time |
| 129 | + Stamping Authority, |
| 130 | +- an append-only transparency log (for example a Merkle-tree log in the style of |
| 131 | + RFC 9162), |
| 132 | +- inclusion in a public blockchain transaction. |
| 133 | + |
| 134 | +Which anchor to use is an operational and threat-model choice. The only thing CDEvents |
| 135 | +tooling needs to agree on is the commitment itself: the canonicalization |
| 136 | +(RFC 8785) and the hash (SHA-256). Everything about the anchor is out of scope for this |
| 137 | +document and MUST NOT be assumed by consumers. |
| 138 | + |
| 139 | +### What the commitment adds beyond DSSE |
| 140 | + |
| 141 | +- Existence proof. Given the event, anyone can recompute the digest and check it against |
| 142 | + the anchor's record, establishing that the event existed by the time the anchor was |
| 143 | + written. |
| 144 | +- Independence from key custody. The check does not require the emitter's signing key or |
| 145 | + its clock. It survives key loss, key rotation, and emitter compromise after the anchor |
| 146 | + time. |
| 147 | +- Reissue detection. Because the digest is bound to a point on the timeline, a later |
| 148 | + re-signed variant of the event produces a different digest with no earlier anchor. |
| 149 | + |
| 150 | +The commitment does not prove authorship. Only the DSSE signature does. The two are |
| 151 | +designed to be used together. |
| 152 | + |
| 153 | +## When to use which |
| 154 | + |
| 155 | +| Requirement | DSSE signature (Part 1) | Hash commitment + anchor (Part 2) | |
| 156 | +| --- | --- | --- | |
| 157 | +| Prove who emitted the event | Yes | No | |
| 158 | +| Detect payload tampering | Yes | Yes (digest mismatch) | |
| 159 | +| Prove the event existed by time T | No | Yes | |
| 160 | +| Verifiable without the emitter's key | No | Yes | |
| 161 | +| Verifiable without trusting the emitter's clock | No | Yes | |
| 162 | +| Detect a later re-signed / back-dated reissue | No | Yes | |
| 163 | +| Extra infrastructure required | A key and key distribution | An external timeline / anchor | |
| 164 | +| Status in this guidance | Recommended baseline | Optional, opt-in | |
| 165 | + |
| 166 | +Baseline: sign CDEvents with DSSE. Add a Part 2 commitment when downstream decisions |
| 167 | +need existence-over-time or must survive emitter key compromise, for example regulated |
| 168 | +audit trails or long-lived provenance. |
| 169 | + |
| 170 | +## Worked example |
| 171 | + |
| 172 | +A runnable end-to-end example accompanies this guide at |
| 173 | +[MarkovianProtocol/cdevents-provenance](https://github.com/MarkovianProtocol/cdevents-provenance). |
| 174 | +It is kept outside this repository so that CDEvents takes no dependency on it. |
| 175 | + |
| 176 | +The example uses real libraries rather than stubs: `rfc8785` for JCS canonicalization, |
| 177 | +`cryptography` for Ed25519, and `jsonschema` to validate the sample event against the |
| 178 | +published CDEvents artifact-published schema. It contains a valid |
| 179 | +`dev.cdevents.artifact.published.0.3.0` event (CDEvents v0.5.1), its JCS canonical |
| 180 | +serialization, the Part 2 commitment, a DSSE envelope over the canonical bytes, and a |
| 181 | +tampered variant of each. Its verifier shows the genuine artifacts passing both checks |
| 182 | +(the signature verifies, and SHA-256 of the signed payload equals the committed hash) |
| 183 | +and the tampered artifacts failing both. |
| 184 | + |
| 185 | +The example is illustrative. It uses one possible anchor mechanism; as stated in Part 2, |
| 186 | +the choice of anchor is out of scope for this guidance. |
| 187 | + |
| 188 | +## References |
| 189 | + |
| 190 | +- DSSE envelope format: secure-systems-lab/dsse, `envelope.md`. |
| 191 | + https://github.com/secure-systems-lab/dsse/blob/master/envelope.md |
| 192 | +- DSSE protocol and PAE: secure-systems-lab/dsse, `protocol.md`. |
| 193 | + https://github.com/secure-systems-lab/dsse/blob/master/protocol.md |
| 194 | +- RFC 8785, JSON Canonicalization Scheme (JCS). https://www.rfc-editor.org/rfc/rfc8785 |
| 195 | +- RFC 4648, Base64 (used by DSSE `payload`/`sig`). https://www.rfc-editor.org/rfc/rfc4648 |
| 196 | +- RFC 3161, Time-Stamp Protocol (one possible anchor for Part 2). |
| 197 | + https://www.rfc-editor.org/rfc/rfc3161 |
| 198 | +- RFC 9162, Certificate Transparency v2 (transparency-log style anchor for Part 2). |
| 199 | + https://www.rfc-editor.org/rfc/rfc9162 |
| 200 | +- CDEvents CloudEvents binding. |
| 201 | + https://github.com/cdevents/spec/blob/main/cloudevents-binding.md |
| 202 | +- CloudEvents specification. https://github.com/cloudevents/spec |
| 203 | +- CloudEvents message-level signature discussion (open). |
| 204 | + https://github.com/cloudevents/spec/issues/565 |
0 commit comments