|
| 1 | + |
| 2 | +.. _index-hosted-attestations: |
| 3 | + |
| 4 | +========================= |
| 5 | +Index hosted attestations |
| 6 | +========================= |
| 7 | + |
| 8 | +.. note:: This specification was originally defined in :pep:`740`. |
| 9 | + |
| 10 | +.. note:: |
| 11 | + |
| 12 | + :pep:`740` includes changes to the HTML and JSON index APIs. |
| 13 | + These changes are documented in the :ref:`simple-repository-api` |
| 14 | + under :ref:`simple-repository-api-base` and :ref:`json-serialization`. |
| 15 | + |
| 16 | +Specification |
| 17 | +============= |
| 18 | + |
| 19 | +.. _upload-endpoint: |
| 20 | + |
| 21 | +Upload endpoint changes |
| 22 | +----------------------- |
| 23 | + |
| 24 | +.. important:: |
| 25 | + |
| 26 | + The "legacy" upload API is not standardized. |
| 27 | + See `PyPI's Upload API documentation <https://docs.pypi.org/api/upload/>`_ |
| 28 | + for how attestations are uploaded. |
| 29 | + |
| 30 | +.. _attestation-object: |
| 31 | + |
| 32 | +Attestation objects |
| 33 | +------------------- |
| 34 | + |
| 35 | +An attestation object is a JSON object with several required keys; applications |
| 36 | +or signers may include additional keys so long as all explicitly |
| 37 | +listed keys are provided. The required layout of an attestation |
| 38 | +object is provided as pseudocode below. |
| 39 | + |
| 40 | +.. code-block:: python |
| 41 | +
|
| 42 | + @dataclass |
| 43 | + class Attestation: |
| 44 | + version: Literal[1] |
| 45 | + """ |
| 46 | + The attestation object's version, which is always 1. |
| 47 | + """ |
| 48 | +
|
| 49 | + verification_material: VerificationMaterial |
| 50 | + """ |
| 51 | + Cryptographic materials used to verify `envelope`. |
| 52 | + """ |
| 53 | +
|
| 54 | + envelope: Envelope |
| 55 | + """ |
| 56 | + The enveloped attestation statement and signature. |
| 57 | + """ |
| 58 | +
|
| 59 | +
|
| 60 | + @dataclass |
| 61 | + class Envelope: |
| 62 | + statement: bytes |
| 63 | + """ |
| 64 | + The attestation statement. |
| 65 | +
|
| 66 | + This is represented as opaque bytes on the wire (encoded as base64), |
| 67 | + but it MUST be an JSON in-toto v1 Statement. |
| 68 | + """ |
| 69 | +
|
| 70 | + signature: bytes |
| 71 | + """ |
| 72 | + A signature for the above statement, encoded as base64. |
| 73 | + """ |
| 74 | +
|
| 75 | + @dataclass |
| 76 | + class VerificationMaterial: |
| 77 | + certificate: str |
| 78 | + """ |
| 79 | + The signing certificate, as `base64(DER(cert))`. |
| 80 | + """ |
| 81 | +
|
| 82 | + transparency_entries: list[object] |
| 83 | + """ |
| 84 | + One or more transparency log entries for this attestation's signature |
| 85 | + and certificate. |
| 86 | + """ |
| 87 | +
|
| 88 | +A full data model for each object in ``transparency_entries`` is provided in |
| 89 | +:ref:`appendix`. Attestation objects **SHOULD** include one or more |
| 90 | +transparency log entries, and **MAY** include additional keys for other |
| 91 | +sources of signed time (such as an :rfc:`3161` Time Stamping Authority or a |
| 92 | +`Roughtime <https://blog.cloudflare.com/roughtime>`__ server). |
| 93 | + |
| 94 | +Attestation objects are versioned; this PEP specifies version 1. Each version |
| 95 | +is tied to a single cryptographic suite to minimize unnecessary cryptographic |
| 96 | +agility. In version 1, the suite is as follows: |
| 97 | + |
| 98 | +* Certificates are specified as X.509 certificates, and comply with the |
| 99 | + profile in :rfc:`5280`. |
| 100 | +* The message signature algorithm is ECDSA, with the P-256 curve for public keys |
| 101 | + and SHA-256 as the cryptographic digest function. |
| 102 | + |
| 103 | +Future PEPs may change this suite (and the overall shape of the attestation |
| 104 | +object) by selecting a new version number. |
| 105 | + |
| 106 | +.. _payload-and-signature-generation: |
| 107 | + |
| 108 | +Attestation statement and signature generation |
| 109 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 110 | + |
| 111 | +The *attestation statement* is the actual claim that is cryptographically signed |
| 112 | +over within the attestation object (i.e., the ``envelope.statement``). |
| 113 | + |
| 114 | +The attestation statement is encoded as a |
| 115 | +`v1 in-toto Statement object <https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/statement.md>`__, |
| 116 | +in JSON form. When serialized the statement is treated as an opaque binary blob, |
| 117 | +avoiding the need for canonicalization. |
| 118 | + |
| 119 | +In addition to being a v1 in-toto Statement, the attestation statement is constrained |
| 120 | +in the following ways: |
| 121 | + |
| 122 | +* The in-toto ``subject`` **MUST** contain only a single subject. |
| 123 | +* ``subject[0].name`` is the distribution's filename, which **MUST** be |
| 124 | + a valid :ref:`source distribution <source-distribution-format>` or |
| 125 | + :ref:`wheel distribution <binary-distribution-format>` filename. |
| 126 | +* ``subject[0].digest`` **MUST** contain a SHA-256 digest. Other digests |
| 127 | + **MAY** be present. The digests **MUST** be represented as hexadecimal strings. |
| 128 | +* The following ``predicateType`` values are supported: |
| 129 | + |
| 130 | + * `SLSA Provenance <https://slsa.dev/provenance/v1>`__: ``https://slsa.dev/provenance/v1`` |
| 131 | + * `PyPI Publish Attestation <https://docs.pypi.org/attestations/publish/v1>`__: ``https://docs.pypi.org/attestations/publish/v1`` |
| 132 | + |
| 133 | +The signature over this statement is constructed using the |
| 134 | +`v1 DSSE signature protocol <https://github.com/secure-systems-lab/dsse/blob/v1.0.0/protocol.md>`__, |
| 135 | +with a ``PAYLOAD_TYPE`` of ``application/vnd.in-toto+json`` and a ``PAYLOAD_BODY`` of the JSON-encoded |
| 136 | +statement above. No other ``PAYLOAD_TYPE`` is permitted. |
| 137 | + |
| 138 | +.. _provenance-object: |
| 139 | + |
| 140 | +Provenance objects |
| 141 | +------------------ |
| 142 | + |
| 143 | +The index will serve uploaded attestations along with metadata that can assist |
| 144 | +in verifying them in the form of JSON serialized objects. |
| 145 | + |
| 146 | +These *provenance objects* will be available via both the Simple Index |
| 147 | +and JSON-based Simple API as described above, and will have the following layout: |
| 148 | + |
| 149 | +.. code-block:: json |
| 150 | +
|
| 151 | + { |
| 152 | + "version": 1, |
| 153 | + "attestation_bundles": [ |
| 154 | + { |
| 155 | + "publisher": { |
| 156 | + "kind": "important-ci-service", |
| 157 | + "claims": {}, |
| 158 | + "vendor-property": "foo", |
| 159 | + "another-property": 123 |
| 160 | + }, |
| 161 | + "attestations": [ |
| 162 | + { /* attestation 1 ... */ }, |
| 163 | + { /* attestation 2 ... */ } |
| 164 | + ] |
| 165 | + } |
| 166 | + ] |
| 167 | + } |
| 168 | +
|
| 169 | +or, as pseudocode: |
| 170 | + |
| 171 | +.. code-block:: python |
| 172 | +
|
| 173 | + @dataclass |
| 174 | + class Publisher: |
| 175 | + kind: string |
| 176 | + """ |
| 177 | + The kind of Trusted Publisher. |
| 178 | + """ |
| 179 | +
|
| 180 | + claims: object | None |
| 181 | + """ |
| 182 | + Any context-specific claims retained by the index during Trusted Publisher |
| 183 | + authentication. |
| 184 | + """ |
| 185 | +
|
| 186 | + _rest: object |
| 187 | + """ |
| 188 | + Each publisher object is open-ended, meaning that it MAY contain additional |
| 189 | + fields beyond the ones specified explicitly above. This field signals that, |
| 190 | + but is not itself present. |
| 191 | + """ |
| 192 | +
|
| 193 | + @dataclass |
| 194 | + class AttestationBundle: |
| 195 | + publisher: Publisher |
| 196 | + """ |
| 197 | + The publisher associated with this set of attestations. |
| 198 | + """ |
| 199 | +
|
| 200 | + attestations: list[Attestation] |
| 201 | + """ |
| 202 | + The set of attestations included in this bundle. |
| 203 | + """ |
| 204 | +
|
| 205 | + @dataclass |
| 206 | + class Provenance: |
| 207 | + version: Literal[1] |
| 208 | + """ |
| 209 | + The provenance object's version, which is always 1. |
| 210 | + """ |
| 211 | +
|
| 212 | + attestation_bundles: list[AttestationBundle] |
| 213 | + """ |
| 214 | + One or more attestation "bundles". |
| 215 | + """ |
| 216 | +
|
| 217 | +* ``version`` is ``1``. Like attestation objects, provenance objects are |
| 218 | + versioned, and this PEP only defines version ``1``. |
| 219 | +* ``attestation_bundles`` is a **required** JSON array, containing one |
| 220 | + or more "bundles" of attestations. Each bundle corresponds to a |
| 221 | + signing identity (such as a Trusted Publishing identity), and contains |
| 222 | + one or more attestation objects. |
| 223 | + |
| 224 | + As noted in the ``Publisher`` model, |
| 225 | + each ``AttestationBundle.publisher`` object is specific to its Trusted Publisher |
| 226 | + but must include at minimum: |
| 227 | + |
| 228 | + * A ``kind`` key, which **MUST** be a JSON string that uniquely identifies the |
| 229 | + kind of Trusted Publisher. |
| 230 | + * A ``claims`` key, which **MUST** be a JSON object containing any context-specific |
| 231 | + claims retained by the index during Trusted Publisher authentication. |
| 232 | + |
| 233 | + All other keys in the publisher object are publisher-specific. |
| 234 | + |
| 235 | + Each array of attestation objects is a superset of the ``attestations`` |
| 236 | + array supplied by the uploaded through the ``attestations`` field at upload |
| 237 | + time, as described in :ref:`upload-endpoint` and |
| 238 | + :ref:`changes-to-provenance-objects`. |
| 239 | + |
| 240 | +.. _changes-to-provenance-objects: |
| 241 | + |
| 242 | +Changes to provenance objects |
| 243 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 244 | + |
| 245 | +Provenance objects are *not* immutable, and may change over time. Reasons |
| 246 | +for changes to the provenance object include but are not limited to: |
| 247 | + |
| 248 | +* Addition of new attestations for a pre-existing signing identity: the index |
| 249 | + **MAY** choose to allow additional attestations by pre-existing signing |
| 250 | + identities, such as newer attestation versions for already uploaded |
| 251 | + files. |
| 252 | + |
| 253 | +* Addition of new signing identities and associated attestations: the index |
| 254 | + **MAY** choose to support attestations from sources other than the file's |
| 255 | + uploader, such as third-party auditors or the index itself. These attestations |
| 256 | + may be performed asynchronously, requiring the index to insert them into |
| 257 | + the provenance object *post facto*. |
| 258 | + |
| 259 | +.. _attestation-verification: |
| 260 | + |
| 261 | +Attestation verification |
| 262 | +------------------------ |
| 263 | + |
| 264 | +Verifying an attestation object against a distribution file requires verification of each of the |
| 265 | +following: |
| 266 | + |
| 267 | +* ``version`` is ``1``. The verifier **MUST** reject any other version. |
| 268 | +* ``verification_material.certificate`` is a valid signing certificate, as |
| 269 | + issued by an *a priori* trusted authority (such as a root of trust already |
| 270 | + present within the verifying client). |
| 271 | +* ``verification_material.certificate`` identifies an appropriate signing |
| 272 | + subject, such as the machine identity of the Trusted Publisher that published |
| 273 | + the package. |
| 274 | +* ``envelope.statement`` is a valid in-toto v1 Statement, with a subject |
| 275 | + and digest that **MUST** match the distribution's filename and contents. |
| 276 | + For the distribution's filename, matching **MUST** be performed by parsing |
| 277 | + using the appropriate source distribution or wheel filename format, as |
| 278 | + the statement's subject may be equivalent but normalized. |
| 279 | +* ``envelope.signature`` is a valid signature for ``envelope.statement`` |
| 280 | + corresponding to ``verification_material.certificate``, |
| 281 | + as reconstituted via the |
| 282 | + `v1 DSSE signature protocol <https://github.com/secure-systems-lab/dsse/blob/v1.0.0/protocol.md>`__. |
| 283 | + |
| 284 | +In addition to the above required steps, a verifier **MAY** additionally verify |
| 285 | +``verification_material.transparency_entries`` on a policy basis, e.g. requiring |
| 286 | +at least one transparency log entry or a threshold of entries. When verifying |
| 287 | +transparency entries, the verifier **MUST** confirm that the inclusion time for |
| 288 | +each entry lies within the signing certificate's validity period. |
| 289 | + |
| 290 | +.. _appendix: |
| 291 | + |
| 292 | +Appendix: Data models for Transparency Log Entries |
| 293 | +==================================================== |
| 294 | + |
| 295 | +This appendix contains pseudocoded data models for transparency log entries |
| 296 | +in attestation objects. Each transparency log entry serves as a source |
| 297 | +of signed inclusion time, and can be verified either online or offline. |
| 298 | + |
| 299 | +.. code-block:: python |
| 300 | +
|
| 301 | + @dataclass |
| 302 | + class TransparencyLogEntry: |
| 303 | + log_index: int |
| 304 | + """ |
| 305 | + The global index of the log entry, used when querying the log. |
| 306 | + """ |
| 307 | +
|
| 308 | + log_id: str |
| 309 | + """ |
| 310 | + An opaque, unique identifier for the log. |
| 311 | + """ |
| 312 | +
|
| 313 | + entry_kind: str |
| 314 | + """ |
| 315 | + The kind (type) of log entry. |
| 316 | + """ |
| 317 | +
|
| 318 | + entry_version: str |
| 319 | + """ |
| 320 | + The version of the log entry's submitted format. |
| 321 | + """ |
| 322 | +
|
| 323 | + integrated_time: int |
| 324 | + """ |
| 325 | + The UNIX timestamp from the log from when the entry was persisted. |
| 326 | + """ |
| 327 | +
|
| 328 | + inclusion_proof: InclusionProof |
| 329 | + """ |
| 330 | + The actual inclusion proof of the log entry. |
| 331 | + """ |
| 332 | +
|
| 333 | +
|
| 334 | + @dataclass |
| 335 | + class InclusionProof: |
| 336 | + log_index: int |
| 337 | + """ |
| 338 | + The index of the entry in the tree it was written to. |
| 339 | + """ |
| 340 | +
|
| 341 | + root_hash: str |
| 342 | + """ |
| 343 | + The digest stored at the root of the Merkle tree at the time of proof |
| 344 | + generation. |
| 345 | + """ |
| 346 | +
|
| 347 | + tree_size: int |
| 348 | + """ |
| 349 | + The size of the Merkle tree at the time of proof generation. |
| 350 | + """ |
| 351 | +
|
| 352 | + hashes: list[str] |
| 353 | + """ |
| 354 | + A list of hashes required to complete the inclusion proof, sorted |
| 355 | + in order from leaf to root. The leaf and root hashes are not themselves |
| 356 | + included in this list; the root is supplied via `root_hash` and the client |
| 357 | + must calculate the leaf hash. |
| 358 | + """ |
| 359 | +
|
| 360 | + checkpoint: str |
| 361 | + """ |
| 362 | + The signed tree head's signature, at the time of proof generation. |
| 363 | + """ |
| 364 | +
|
| 365 | + cosigned_checkpoints: list[str] |
| 366 | + """ |
| 367 | + Cosigned checkpoints from zero or more log witnesses. |
| 368 | + """ |
0 commit comments