Skip to content

Commit dd04e6d

Browse files
authored
Merge pull request #1646 from woodruffw-forks/ww/pep-740
specifications: create living copy of PEP 740
2 parents 80ba369 + dcd35fd commit dd04e6d

File tree

4 files changed

+386
-1
lines changed

4 files changed

+386
-1
lines changed

source/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@
194194
"tox": ("https://tox.wiki/en/latest/", None),
195195
"twine": ("https://twine.readthedocs.io/en/stable/", None),
196196
"virtualenv": ("https://virtualenv.pypa.io/en/stable/", None),
197+
"warehouse": ("https://warehouse.pypa.io/", None),
197198
}
198199

199200
# -- Options for todo extension --------------------------------------------------------
Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
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+
"""

source/specifications/section-package-indices.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Package Index Interfaces
77

88
pypirc
99
simple-repository-api
10+
index-hosted-attestations

0 commit comments

Comments
 (0)