1084 lines
56 KiB
Plaintext
1084 lines
56 KiB
Plaintext
|
PEP: 458
|
||
|
Title: Surviving a Compromise of PyPI
|
||
|
Version: $Revision$
|
||
|
Last-Modified: $Date$
|
||
|
Author: Trishank Karthik Kuppusamy <tk47@students.poly.edu>,
|
||
|
Donald Stufft <donald@stufft.io>,
|
||
|
Justin Cappos <jcappos@poly.edu>
|
||
|
Discussions-To: Distutils SIG <distutils-sig@python.org>
|
||
|
Status: Draft
|
||
|
Type: Standards Track
|
||
|
Content-Type: text/x-rst
|
||
|
Created: 27-Sep-2013
|
||
|
|
||
|
|
||
|
Abstract
|
||
|
========
|
||
|
|
||
|
This PEP describes how the Python Package Index (PyPI [1]_) may be integrated
|
||
|
with The Update Framework [2]_ (TUF). TUF was designed to be a plug-and-play
|
||
|
security add-on to a software updater or package manager. TUF provides
|
||
|
end-to-end security like SSL, but for software updates instead of HTTP
|
||
|
connections. The framework integrates best security practices such as
|
||
|
separating responsibilities, adopting the many-man rule for signing packages,
|
||
|
keeping signing keys offline, and revocation of expired or compromised signing
|
||
|
keys.
|
||
|
|
||
|
The proposed integration will render modern package managers such as pip [3]_
|
||
|
more secure against various types of security attacks on PyPI and protect users
|
||
|
against them. Even in the worst case where an attacker manages to compromise
|
||
|
PyPI itself, the damage is controlled in scope and limited in duration.
|
||
|
|
||
|
Specifically, this PEP will describe how PyPI processes should be adapted to
|
||
|
incorporate TUF metadata. It will not prescribe how package managers such as
|
||
|
pip should be adapted to install or update with TUF metadata projects from
|
||
|
PyPI.
|
||
|
|
||
|
|
||
|
Rationale
|
||
|
=========
|
||
|
|
||
|
In January 2013, the Python Software Foundation (PSF) announced [4]_ that the
|
||
|
python.org wikis for Python, Jython, and the PSF were subjected to a security
|
||
|
breach which caused all of the wiki data to be destroyed on January 5 2013.
|
||
|
Fortunately, the PyPI infrastructure was not affected by this security breach.
|
||
|
However, the incident is a reminder that PyPI should take defensive steps to
|
||
|
protect users as much as possible in the event of a compromise. Attacks on
|
||
|
software repositories happen all the time [5]_. We must accept the possibility
|
||
|
of security breaches and prepare PyPI accordingly because it is a valuable
|
||
|
target used by thousands, if not millions, of people.
|
||
|
|
||
|
Before the wiki attack, PyPI used MD5 hashes to tell package managers such as
|
||
|
pip whether or not a package was corrupted in transit. However, the absence of
|
||
|
SSL made it hard for package managers to verify transport integrity to PyPI.
|
||
|
It was easy to launch a man-in-the-middle attack between pip and PyPI to change
|
||
|
package contents arbitrarily. This can be used to trick users into installing
|
||
|
malicious packages. After the wiki attack, several steps were proposed (some
|
||
|
of which were implemented) to deliver a much higher level of security than was
|
||
|
previously the case: requiring SSL to communicate with PyPI [6]_, restricting
|
||
|
project names [7]_, and migrating from MD5 to SHA-2 hashes [8]_.
|
||
|
|
||
|
These steps, though necessary, are insufficient because attacks are still
|
||
|
possible through other avenues. For example, a public mirror is trusted to
|
||
|
honestly mirror PyPI, but some mirrors may misbehave due to malice or accident.
|
||
|
Package managers such as pip are supposed to use signatures from PyPI to verify
|
||
|
packages downloaded from a public mirror [9]_, but none are known to actually
|
||
|
do so [10]_. Therefore, it is also wise to add more security measures to
|
||
|
detect attacks from public mirrors or content delivery networks [11]_ (CDNs).
|
||
|
|
||
|
Even though official mirrors are being deprecated on PyPI [12]_, there remain a
|
||
|
wide variety of other attack vectors on package managers [13]_. Among other
|
||
|
things, these attacks can crash client systems, cause obsolete packages to be
|
||
|
installed, or even allow an attacker to execute arbitrary code. In September
|
||
|
2013, we showed how the latest version of pip then was susceptible to these
|
||
|
attacks and how TUF could protect users against them [14]_.
|
||
|
|
||
|
Finally, PyPI allows for packages to be signed with GPG keys [15]_, although no
|
||
|
package manager is known to verify those signatures, thus negating much of the
|
||
|
benefits of having those signatures at all. Validating integrity through
|
||
|
cryptography is important, but issues such as immediate and secure key
|
||
|
revocation or specifying a required threshold number of signatures still
|
||
|
remain. Furthermore, GPG by itself does not immediately address the attacks
|
||
|
mentioned above.
|
||
|
|
||
|
In order to protect PyPI against infrastructure compromises, we propose
|
||
|
integrating PyPI with The Update Framework [2]_ (TUF).
|
||
|
|
||
|
|
||
|
Definitions
|
||
|
===========
|
||
|
|
||
|
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD",
|
||
|
"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be
|
||
|
interpreted as described in RFC 2119__.
|
||
|
|
||
|
__ http://www.ietf.org/rfc/rfc2119.txt
|
||
|
|
||
|
In order to keep this PEP focused solely on the application of TUF on PyPI, the
|
||
|
reader is assumed to already be familiar with the design principles of
|
||
|
TUF [2]_. It is also strongly RECOMMENDED that the reader be familiar with the
|
||
|
TUF specification [16]_.
|
||
|
|
||
|
* Projects: Projects are software components that are made available for
|
||
|
integration. Projects include Python libraries, frameworks, scripts, plugins,
|
||
|
applications, collections of data or other resources, and various
|
||
|
combinations thereof. Public Python projects are typically registered on the
|
||
|
Python Package Index [17]_.
|
||
|
|
||
|
* Releases: Releases are uniquely identified snapshots of a project [17]_.
|
||
|
|
||
|
* Distributions: Distributions are the packaged files which are used to publish
|
||
|
and distribute a release [17]_.
|
||
|
|
||
|
* Simple index: The HTML page which contains internal links to the
|
||
|
distributions of a project [17]_.
|
||
|
|
||
|
* Consistent snapshot: A set of TUF metadata and PyPI targets that capture the
|
||
|
complete state of all projects on PyPI as they were at some fixed point in
|
||
|
time.
|
||
|
|
||
|
* The *consistent-snapshot* (*release*) role: In order to prevent confusion due
|
||
|
to the different meanings of the term "release" as employed by PEP 426 [17]_
|
||
|
and the TUF specification [16]_, we rename the *release* role as the
|
||
|
*consistent-snapshot* role.
|
||
|
|
||
|
* Continuous delivery: A set of processes with which PyPI produces consistent
|
||
|
snapshots that can safely coexist and deleted independently [18]_.
|
||
|
|
||
|
* Developer: Either the owner or maintainer of a project who is allowed to
|
||
|
update the TUF metadata as well as distribution metadata and data for the
|
||
|
project.
|
||
|
|
||
|
* Online key: A key that MUST be stored on the PyPI server infrastructure.
|
||
|
This is usually to allow automated signing with the key. However, this means
|
||
|
that an attacker who compromises PyPI infrastructure will be able to read
|
||
|
these keys.
|
||
|
|
||
|
* Offline key: A key that MUST be stored off the PyPI infrastructure. This
|
||
|
prevents automated signing with the key. This means that an attacker who
|
||
|
compromises PyPI infrastructure will not be able to immediately read these
|
||
|
keys.
|
||
|
|
||
|
* Developer key: A private key for which its corresponding public key is
|
||
|
registered with PyPI to say that it is responsible for directly signing for
|
||
|
or delegating the distributions belonging to a project. For the purposes of
|
||
|
this PEP, it is offline in the sense that the private key MUST not be stored
|
||
|
on PyPI. However, the project is free to require certain developer keys to
|
||
|
be online on its own infrastructure.
|
||
|
|
||
|
* Threshold signature scheme: A role could increase its resilience to key
|
||
|
compromises by requiring that at least t out of n keys are REQUIRED to sign
|
||
|
its metadata. This means that a compromise of t-1 keys is insufficient to
|
||
|
compromise the role itself. We denote this property by saying that the role
|
||
|
requires (t, n) keys.
|
||
|
|
||
|
|
||
|
Overview
|
||
|
========
|
||
|
|
||
|
.. image:: https://raw.github.com/theupdateframework/pep-on-pypi-with-tuf/master/figure1.png
|
||
|
|
||
|
Figure 1: A simplified overview of the roles in PyPI with TUF
|
||
|
|
||
|
Figure 1 shows a simplified overview of the roles that TUF metadata assume on
|
||
|
PyPI. The top-level *root* role signs for the keys of the top-level
|
||
|
*timestamp*, *consistent-snapshot*, *targets* and *root* roles. The
|
||
|
*timestamp* role signs for a new and consistent snapshot. The *consistent-
|
||
|
snapshot* role signs for the *root*, *targets* and all delegated targets
|
||
|
metadata. The *claimed* role signs for all projects that have registered their
|
||
|
own developer keys with PyPI. The *recently-claimed* role signs for all
|
||
|
projects that recently registered their own developer keys with PyPI. Finally,
|
||
|
the *unclaimed* role signs for all projects that have not registered developer
|
||
|
keys with PyPI. The *claimed*, *recently-claimed* and *unclaimed* roles are
|
||
|
numbered 1, 2, 3 respectively because a project will be searched for in each of
|
||
|
those roles in that descending order: first in *claimed*, then in
|
||
|
*recently-claimed* if necessary, and finally in *unclaimed* if necessary.
|
||
|
|
||
|
Every year, PyPI administrators are going to sign for *root* role keys. After
|
||
|
that, automation will continuously sign for a timestamped, consistent snapshot
|
||
|
of all projects. Every few months, PyPI administrators will move projects with
|
||
|
vetted developer keys from the *recently-claimed* role to the *claimed* role.
|
||
|
As we will soon see, they will sign for *claimed* with projects with offline
|
||
|
keys.
|
||
|
|
||
|
This PEP does not require project developers to use TUF to secure their
|
||
|
packages from attacks on PyPI. By default, all projects will be signed for by
|
||
|
the *unclaimed* role. If a project wishes stronger security guarantees, then
|
||
|
the project is strongly RECOMMENDED to register developer keys with PyPI so
|
||
|
that it may sign for its own distributions. By doing so, the project must
|
||
|
remain as a *recently-claimed* project until PyPI administrators have had an
|
||
|
opportunity to vet the developer keys of the project, after which the project
|
||
|
will be moved to the *claimed* role.
|
||
|
|
||
|
This PEP has **not** been designed to be backward-compatible for package
|
||
|
managers that do not use the TUF security protocol to install or update a
|
||
|
project from the PyPI described here. Instead, it is RECOMMENDED that PyPI
|
||
|
maintain a backward-compatible API of itself that does NOT offer TUF so that
|
||
|
older package managers that do not use TUF will be able to install or update
|
||
|
projects from PyPI as usual but without any of the security offered by TUF.
|
||
|
For the rest of this PEP, we will assume that PyPI will simultaneously maintain
|
||
|
a backward-incompatible API of itself for package managers that MUST use TUF to
|
||
|
securely install or update projects. We think that this approach represents a
|
||
|
reasonable trade-off: older package managers that do not TUF will still be able
|
||
|
to install or update projects without any TUF security from PyPI, and newer
|
||
|
package managers that do use TUF will be able to securely install or update
|
||
|
projects. At some point in the future, PyPI administrators MAY choose to
|
||
|
permanently deprecate the backward-compatible version of itself that does not
|
||
|
offer TUF metadata.
|
||
|
|
||
|
Unless a mirror, CDN or the PyPI repository has been compromised, the end-user
|
||
|
will not be able to discern whether or not a package manager is using TUF to
|
||
|
install or update a project from PyPI.
|
||
|
|
||
|
|
||
|
Responsibility Separation
|
||
|
=========================
|
||
|
|
||
|
Recall that TUF requires four top-level roles: *root*, *timestamp*,
|
||
|
*consistent-snapshot* and *targets*. The *root* role specifies the keys of all
|
||
|
the top-level roles (including itself). The *timestamp* role specifies the
|
||
|
latest consistent snapshot. The *consistent-snapshot* role specifies the
|
||
|
latest versions of all TUF metadata files (other than *timestamp*). The
|
||
|
*targets* role specifies available target files (in our case, it will be all
|
||
|
files on PyPI under the /simple and /packages directories). In this PEP, each
|
||
|
of these roles will serve their responsibilities without exception.
|
||
|
|
||
|
Our proposal offers two levels of security to developers. If developers opt in
|
||
|
to secure their projects with their own developer keys, then their projects
|
||
|
will be very secure. Otherwise, TUF will still protect them in many cases:
|
||
|
|
||
|
1. Minimum security (no action by a developer): protects *unclaimed* and
|
||
|
*recently-claimed* projects without developer keys from CDNs [19]_ or public
|
||
|
mirrors, but not from some PyPI compromises. This is because continuous
|
||
|
delivery requires some keys to be online. This level of security protects
|
||
|
projects from being accidentally or deliberately tampered with by a mirror
|
||
|
or a CDN because the mirror or CDN will not have any of the PyPI or
|
||
|
developer keys required to sign for projects. However, it would not protect
|
||
|
projects from attackers who have compromised PyPI because they will be able
|
||
|
to manipulate the TUF metadata for *unclaimed* projects with the appropriate
|
||
|
online keys.
|
||
|
|
||
|
2. Maximum security (developer signs their project): protects projects with
|
||
|
developer keys not only from CDNs or public mirrors, but also from some PyPI
|
||
|
compromises. This is because many important keys will be offline. This
|
||
|
level of security protects projects from being accidentally or deliberately
|
||
|
tampered with by a mirror or a CDN for reasons identical to the minimum
|
||
|
security level. It will also protect projects (or at least mitigate
|
||
|
damages) from the most likely attacks on PyPI. For example: given access to
|
||
|
online keys after a PyPI compromise, attackers will be able to freeze the
|
||
|
distributions for these projects, but they will not be able to serve
|
||
|
malicious distributions for these projects (not without compromising other
|
||
|
offline keys which would entail more risk, time and energy). Details for
|
||
|
the exact level of security offered is discussed in the section on key
|
||
|
management.
|
||
|
|
||
|
In order to complete support for continuous delivery, we propose three
|
||
|
delegated targets roles:
|
||
|
|
||
|
1. *claimed*: Signs for the delegation of PyPI projects to their respective
|
||
|
developer keys.
|
||
|
|
||
|
2. *recently-claimed*: This role is almost identical to the *claimed* role and
|
||
|
could technically be performed by the *unclaimed* role, but there are two
|
||
|
important reasons why it exists independently: the first reason is to
|
||
|
improve the performance of looking up projects in the *unclaimed* role (by
|
||
|
moving metadata to the *recently-claimed* role instead), and the second
|
||
|
reason is to make it easier for PyPI administrators to move
|
||
|
*recently-claimed* projects to the *claimed* role.
|
||
|
|
||
|
3. *unclaimed*: Signs for PyPI projects without developer keys.
|
||
|
|
||
|
The *targets* role MUST delegate all PyPI projects to the three delegated
|
||
|
targets roles in the order of appearance listed above. This means that when
|
||
|
pip downloads with TUF a distribution from a project on PyPI, it will first
|
||
|
consult the *claimed* role about it. If the *claimed* role has delegated the
|
||
|
project, then pip will trust the project developers (in order of delegation)
|
||
|
about the TUF metadata for the project. Otherwise, pip will consult the
|
||
|
*recently-claimed* role about the project. If the *recently-claimed* role has
|
||
|
delegated the project, then pip will trust the project developers (in order of
|
||
|
delegation) about the TUF metadata for the project. Otherwise, pip will
|
||
|
consult the *unclaimed* role about the TUF metadata for the project. If the
|
||
|
*unclaimed* role has not delegated the project, then the project is considered
|
||
|
to be non-existent on PyPI.
|
||
|
|
||
|
A PyPI project MAY begin without registering a developer key. Therefore, the
|
||
|
project will be signed for by the *unclaimed* role. After registering
|
||
|
developer keys, the project will be removed from the *unclaimed* role and
|
||
|
delegated to the *recently-claimed* role. After a probation period and a
|
||
|
vetting process to verify the developer keys of the project, the project will
|
||
|
be removed from the *recently-claimed* role and delegated to the *claimed*
|
||
|
role.
|
||
|
|
||
|
The *claimed* role offers maximum security, whereas the *recently-claimed* and
|
||
|
*unclaimed* role offer minimum security. All three roles support continuous
|
||
|
delivery of PyPI projects.
|
||
|
|
||
|
The *unclaimed* role offers minimum security because PyPI will sign for
|
||
|
projects without developer keys with an online key in order to permit
|
||
|
continuous delivery.
|
||
|
|
||
|
The *recently-claimed* role offers minimum security because while the project
|
||
|
developers will sign for their own distributions with offline developer keys,
|
||
|
PyPI will sign with an online key the delegation of the project to those
|
||
|
offline developer keys. The signing of the delegation with an online key
|
||
|
allows PyPI administrators to continuously deliver projects without having to
|
||
|
continuously sign the delegation whenever one of those projects registers
|
||
|
developer keys.
|
||
|
|
||
|
Finally, the *claimed* role offers maximum security because PyPI will sign with
|
||
|
offline keys the delegation of a project to its offline developer keys. This
|
||
|
means that every now and then, PyPI administrators will vet developer keys and
|
||
|
sign the delegation of a project to those developer keys after being reasonably
|
||
|
sure about the ownership of the developer keys. The process for vetting
|
||
|
developer keys is out of the scope of this PEP.
|
||
|
|
||
|
|
||
|
Metadata Management
|
||
|
===================
|
||
|
|
||
|
In this section, we examine the TUF metadata that PyPI must manage by itself,
|
||
|
and other TUF metadata that must be safely delegated to projects. Examples of
|
||
|
the metadata described here may be seen at our testbed mirror of
|
||
|
`PyPI-with-TUF`__.
|
||
|
|
||
|
__ http://mirror1.poly.edu/
|
||
|
|
||
|
The metadata files that change most frequently will be *timestamp*,
|
||
|
*consistent-snapshot* and delegated targets (*claimed*, *recently-claimed*,
|
||
|
*unclaimed*, project) metadata. The *timestamp* and *consistent-snapshot*
|
||
|
metadata MUST be updated whenever *root*, *targets* or delegated targets
|
||
|
metadata are updated. Observe, though, that *root* and *targets* metadata are
|
||
|
much less likely to be updated as often as delegated targets metadata.
|
||
|
Therefore, *timestamp* and *consistent-snapshot* metadata will most likely be
|
||
|
updated frequently (possibly every minute) due to delegated targets metadata
|
||
|
being updated frequently in order to drive continuous delivery of projects.
|
||
|
|
||
|
Consequently, the processes with which PyPI updates projects will have to be
|
||
|
updated accordingly, the details of which are explained in the following
|
||
|
subsections.
|
||
|
|
||
|
|
||
|
Why Do We Need Consistent Snapshots?
|
||
|
------------------------------------
|
||
|
|
||
|
In an ideal world, metadata and data should be immediately updated and
|
||
|
presented whenever a project is updated. In practice, there will be problems
|
||
|
when there are many readers and writers who access the same metadata or data at
|
||
|
the same time.
|
||
|
|
||
|
An important example at the time of writing is that, mirrors are very likely,
|
||
|
as far as we can tell, to update in an inconsistent manner from PyPI as it is
|
||
|
without TUF. Specifically, a mirror would update itself in such a way that
|
||
|
project A would be from time T, whereas project B would be from time T+5,
|
||
|
project C would be from time T+3, and so on where T is the time that the mirror
|
||
|
first begun updating itself. There is no known way for a mirror to update
|
||
|
itself such that it captures the state of all projects as they were at time T.
|
||
|
|
||
|
Adding TUF to PyPI will not automatically solve the problem. Consider what we
|
||
|
call the `"inverse replay" or "fast-forward" problem`__. Suppose that PyPI has
|
||
|
timestamped a consistent snapshot at version 1. A mirror is later in the
|
||
|
middle of copying PyPI at this snapshot. While the mirror is copying PyPI at
|
||
|
this snapshot, PyPI timestamps a new snapshot at, say, version 2. Without
|
||
|
accounting for consistency, the mirror would then find itself with a copy of
|
||
|
PyPI in an inconsistent state which is indistinguishable from arbitrary
|
||
|
metadata or target attacks. The problem would also apply when the mirror is
|
||
|
substituted with a pip user.
|
||
|
|
||
|
__ https://groups.google.com/forum/#!topic/theupdateframework/8mkR9iqivQA
|
||
|
|
||
|
Therefore, the problem can be summarized as such: there are problems of
|
||
|
consistency on PyPI with or without TUF. TUF requires its metadata to be
|
||
|
consistent with the data, but how would the metadata be kept consistent with
|
||
|
projects that change all the time?
|
||
|
|
||
|
As a result, we will solve for PyPI the problem of producing a consistent
|
||
|
snapshot that captures the state of all known projects at a given time. Each
|
||
|
consistent snapshot can safely coexist with any other consistent snapshot and
|
||
|
deleted independently without affecting any other consistent snapshot.
|
||
|
|
||
|
The gist of the solution is that every metadata or data file written to disk
|
||
|
MUST include in its filename the `cryptographic hash`__ of the file. How would
|
||
|
this help clients which use the TUF protocol to securely and consistently
|
||
|
install or update a project from PyPI?
|
||
|
|
||
|
__ https://en.wikipedia.org/wiki/Cryptographic_hash_function
|
||
|
|
||
|
Recall that the first step in the TUF protocol requires the client to download
|
||
|
the latest *timestamp* metadata. However, the client would not know in advance
|
||
|
the hash of the *timestamp* metadata file from the latest consistent snapshot.
|
||
|
Therefore, PyPI MUST redirect all HTTP GET requests for *timestamp* metadata to
|
||
|
the *timestamp* metadata file from the latest consistent snapshot. Since the
|
||
|
*timestamp* metadata is the root of a tree of cryptographic hashes pointing to
|
||
|
every other metadata or target file that are meant to exist together for
|
||
|
consistency, the client is then able to retrieve any file from this consistent
|
||
|
snapshot by deterministically including, in the request for the file, the hash
|
||
|
of the file in the filename. Assuming infinite disk space and no `hash
|
||
|
collisions`__, a client may safely read from one consistent snapshot while PyPI
|
||
|
produces another consistent snapshot.
|
||
|
|
||
|
__ https://en.wikipedia.org/wiki/Collision_(computer_science)
|
||
|
|
||
|
In this simple but effective manner, we are able to capture a consistent
|
||
|
snapshot of all projects and the associated metadata at a given time. The next
|
||
|
subsection will explicate the implementation details of this idea.
|
||
|
|
||
|
|
||
|
Producing Consistent Snapshots
|
||
|
------------------------------
|
||
|
|
||
|
Given a project, PyPI is responsible for updating, depending on the project,
|
||
|
either the *claimed*, *recently-claimed* or *unclaimed* metadata as well as
|
||
|
associated delegated targets metadata. Every project MUST upload its set of
|
||
|
metadata and targets in a single transaction. We will call this set of files
|
||
|
the project transaction. We will discuss later how PyPI MAY validate the files
|
||
|
in a project transaction. For now, let us focus on how PyPI will respond to a
|
||
|
project transaction. We will call this response the project transaction
|
||
|
process. There will also be a consistent snapshot process that we will define
|
||
|
momentarily; for now, it suffices to know that project transaction processes
|
||
|
and the consistent snapshot process must coordinate with each other.
|
||
|
|
||
|
Also, every metadata and target file MUST include in its filename the `hex
|
||
|
digest`__ of its `SHA-256`__ hash. For this PEP, it is RECOMMENDED that PyPI
|
||
|
adopt a simple convention of the form filename.digest.ext, where filename is
|
||
|
the original filename without a copy of the hash, digest is the hex digest of
|
||
|
the hash, and ext is the filename extension.
|
||
|
|
||
|
__ http://docs.python.org/2/library/hashlib.html#hashlib.hash.hexdigest
|
||
|
__ https://en.wikipedia.org/wiki/SHA-2
|
||
|
|
||
|
When an *unclaimed* project uploads a new transaction, a project transaction
|
||
|
process MUST add all new targets and relevant delegated *unclaimed* metadata.
|
||
|
(We will see later in this section why the *unclaimed* role will delegate
|
||
|
targets to a number of delegated *unclaimed* roles.) Finally, the project
|
||
|
transaction process MUST inform the consistent snapshot process about new
|
||
|
delegated *unclaimed* metadata.
|
||
|
|
||
|
When a *recently-claimed* project uploads a new a transaction, a project
|
||
|
transaction process MUST add all new targets and delegated targets metadata for
|
||
|
the project. If the project is new, then the project transaction process MUST
|
||
|
also add new *recently-claimed* metadata with public keys and threshold number
|
||
|
(which MUST be part of the transaction) for the project. Finally, the project
|
||
|
transaction process MUST inform the consistent snapshot process about new
|
||
|
*recently-claimed* metadata as well as the current set of delegated targets
|
||
|
metadata for the project.
|
||
|
|
||
|
The process for a *claimed* project is slightly different. The difference is
|
||
|
that PyPI administrators will choose to move the project from the
|
||
|
*recently-claimed* role to the *claimed* role. A project transaction process
|
||
|
MUST then add new *recently-claimed* and *claimed* metadata to reflect this
|
||
|
migration. As is the case for a *recently-claimed* project, the project
|
||
|
transaction process MUST always add all new targets and delegated targets
|
||
|
metadata for the *claimed* project. Finally, the project transaction process
|
||
|
MUST inform the consistent snapshot process about new *recently-claimed* or
|
||
|
*claimed* metadata as well as the current set of delegated targets metadata for
|
||
|
the project.
|
||
|
|
||
|
Project transaction processes SHOULD be automated, except when PyPI
|
||
|
administrators move a project from the *recently-claimed* role to the *claimed*
|
||
|
role. Project transaction processes MUST also be applied atomically: either
|
||
|
all metadata and targets, or none of them, are added. The project transaction
|
||
|
processes and consistent snapshot process SHOULD work concurrently. Finally,
|
||
|
project transaction processes SHOULD keep in memory the latest *claimed*,
|
||
|
*recently-claimed* and *unclaimed* metadata so that they will be correctly
|
||
|
updated in new consistent snapshots.
|
||
|
|
||
|
All project transactions MAY be placed in a single queue and processed
|
||
|
serially. Alternatively, the queue MAY be processed concurrently in order of
|
||
|
appearance provided that the following rules are observed:
|
||
|
|
||
|
1. No pair of project transaction processes must concurrently work on the same
|
||
|
project.
|
||
|
|
||
|
2. No pair of project transaction processes must concurrently work on
|
||
|
*unclaimed* projects that belong to the same delegated *unclaimed* targets
|
||
|
role.
|
||
|
|
||
|
3. No pair of project transaction processes must concurrently work on new
|
||
|
*recently-claimed* projects.
|
||
|
|
||
|
4. No pair of project transaction processes must concurrently work on new
|
||
|
*claimed* projects.
|
||
|
|
||
|
5. No project transaction process must work on a new *claimed* project while
|
||
|
another project transaction process is working on a new *recently-claimed*
|
||
|
project and vice versa.
|
||
|
|
||
|
These rules MUST be observed so that metadata is not read from or written to
|
||
|
inconsistently.
|
||
|
|
||
|
The consistent snapshot process is fairly simple and SHOULD be automated. The
|
||
|
consistent snapshot process MUST keep in memory the latest working set of
|
||
|
*root*, *targets* and delegated targets metadata. Every minute or so, the
|
||
|
consistent snapshot process will sign for this latest working set. (Recall
|
||
|
that project transaction processes continuously inform the consistent snapshot
|
||
|
process about the latest delegated targets metadata in a concurrency-safe
|
||
|
manner. The consistent snapshot process will actually sign for a copy of the
|
||
|
latest working set while the actual latest working set in memory will be
|
||
|
updated with information continuously communicated by project transaction
|
||
|
processes.) Next, the consistent snapshot process MUST generate and sign new
|
||
|
*timestamp* metadata that will vouch for the *consistent-snapshot* metadata
|
||
|
generated in the previous step. Finally, the consistent snapshot process MUST
|
||
|
add new *timestamp* and *consistent-snapshot* metadata representing the latest
|
||
|
consistent snapshot.
|
||
|
|
||
|
A few implementation notes are now in order. So far, we have seen only that
|
||
|
new metadata and targets are added, but not that old metadata and targets are
|
||
|
removed. Practical constraints are such that eventually PyPI will run out of
|
||
|
disk space to produce a new consistent snapshot. In that case, PyPI MAY then
|
||
|
use something like a "mark-and-sweep" algorithm to delete sufficiently old
|
||
|
consistent snapshots: in order to preserve the latest consistent snapshot, PyPI
|
||
|
would walk objects beginning from the root (*timestamp*) of the latest
|
||
|
consistent snapshot, mark all visited objects, and delete all unmarked
|
||
|
objects. The last few consistent snapshots may be preserved in a similar
|
||
|
fashion. Deleting a consistent snapshot will cause clients to see nothing
|
||
|
thereafter but HTTP 404 responses to any request for a file in that consistent
|
||
|
snapshot. Clients SHOULD then retry their requests with the latest consistent
|
||
|
snapshot.
|
||
|
|
||
|
We do **not** consider updates to any consistent snapshot because `hash
|
||
|
collisions`__ are out of the scope of this PEP. In case a hash collision is
|
||
|
observed, PyPI MAY wish to check that the file being added is identical to the
|
||
|
file already stored. (Should a hash collision be observed, it is far more
|
||
|
likely the case that the file is identical rather than being a genuine
|
||
|
`collision attack`__.) Otherwise, PyPI MAY either overwrite the existing file
|
||
|
or ignore any write operation to an existing file.
|
||
|
|
||
|
__ https://en.wikipedia.org/wiki/Collision_(computer_science)
|
||
|
__ https://en.wikipedia.org/wiki/Collision_attack
|
||
|
|
||
|
All clients, such as pip using the TUF protocol, MUST be modified to download
|
||
|
every metadata and target file (except for *timestamp* metadata) by including,
|
||
|
in the request for the file, the hash of the file in the filename. Following
|
||
|
the filename convention recommended earlier, a request for the file at
|
||
|
filename.ext will be transformed to the equivalent request for the file at
|
||
|
filename.digest.ext.
|
||
|
|
||
|
Finally, PyPI SHOULD use a `transaction log`__ to record project transaction
|
||
|
processes and queues so that it will be easier to recover from errors after a
|
||
|
server failure.
|
||
|
|
||
|
__ https://en.wikipedia.org/wiki/Transaction_log
|
||
|
|
||
|
|
||
|
Metadata Validation
|
||
|
-------------------
|
||
|
|
||
|
A *claimed* or *recently-claimed* project will need to upload in its
|
||
|
transaction to PyPI not just targets (a simple index as well as distributions)
|
||
|
but also TUF metadata. The project MAY do so by uploading a ZIP file
|
||
|
containing two directories, /metadata/ (containing delegated targets metadata
|
||
|
files) and /targets/ (containing targets such as the project simple index and
|
||
|
distributions which are signed for by the delegated targets metadata).
|
||
|
|
||
|
Whenever the project uploads metadata or targets to PyPI, PyPI SHOULD check the
|
||
|
project TUF metadata for at least the following properties:
|
||
|
|
||
|
* A threshold number of the developers keys registered with PyPI by that
|
||
|
project MUST have signed for the delegated targets metadata file that
|
||
|
represents the "root" of targets for that project (e.g. metadata/targets/
|
||
|
project.txt).
|
||
|
|
||
|
* The signatures of delegated targets metadata files MUST be valid.
|
||
|
|
||
|
* The delegated targets metadata files MUST NOT be expired.
|
||
|
|
||
|
* The delegated targets metadata MUST be consistent with the targets.
|
||
|
|
||
|
* A delegator MUST NOT delegate targets that were not delegated to itself by
|
||
|
another delegator.
|
||
|
|
||
|
* A delegatee MUST NOT sign for targets that were not delegated to itself by a
|
||
|
delegator.
|
||
|
|
||
|
* Every file MUST contain a unique copy of its hash in its filename following
|
||
|
the filename.digest.ext convention recommended earlier.
|
||
|
|
||
|
If PyPI chooses to check the project TUF metadata, then PyPI MAY choose to
|
||
|
reject publishing any set of metadata or targets that do not meet these
|
||
|
requirements.
|
||
|
|
||
|
PyPI MUST enforce access control by ensuring that each project can only write
|
||
|
to the TUF metadata for which it is responsible. It MUST do so by ensuring
|
||
|
that project transaction processes write to the correct metadata as well as
|
||
|
correct locations within those metadata. For example, a project transaction
|
||
|
process for an *unclaimed* project MUST write to the correct target paths in
|
||
|
the correct delegated *unclaimed* metadata for the targets of the project.
|
||
|
|
||
|
On rare occasions, PyPI MAY wish to extend the TUF metadata format for projects
|
||
|
in a backward-incompatible manner. Note that PyPI will NOT be able to
|
||
|
automatically rewrite existing TUF metadata on behalf of projects in order to
|
||
|
upgrade the metadata to the new backward-incompatible format because this would
|
||
|
invalidate the signatures of the metadata as signed by developer keys.
|
||
|
Instead, package managers SHOULD be written to recognize and handle multiple
|
||
|
incompatible versions of TUF metadata so that *claimed* and *recently-claimed*
|
||
|
projects could be offered a reasonable time to migrate their metadata to newer
|
||
|
but backward-incompatible formats.
|
||
|
|
||
|
The details of how each project manages its TUF metadata is beyond the scope of
|
||
|
this PEP.
|
||
|
|
||
|
|
||
|
Mirroring Protocol
|
||
|
------------------
|
||
|
|
||
|
The mirroring protocol as described in PEP 381 [9]_ SHOULD change to mirror
|
||
|
PyPI with TUF.
|
||
|
|
||
|
A mirror SHOULD have to maintain for its clients only one consistent snapshot
|
||
|
which would represent the latest consistent snapshot from PyPI known to the
|
||
|
mirror. The mirror would then serve all HTTP requests for metadata or targets
|
||
|
by simply reading directly from this consistent snapshot directory.
|
||
|
|
||
|
The mirroring protocol itself is fairly simple. The mirror would ask PyPI for
|
||
|
*timestamp* metadata from the latest consistent snapshot and proceed to copy
|
||
|
the entire consistent snapshot from the *timestamp* metadata onwards. If the
|
||
|
mirror encounters a failure to copy any metadata or target file while copying
|
||
|
the consistent snapshot, it SHOULD retrying resuming the copy of that
|
||
|
particular consistent snapshot. If PyPI has deleted that consistent snapshot,
|
||
|
then the mirror SHOULD delete the failed consistent snapshot and try
|
||
|
downloading the latest consistent snapshot instead.
|
||
|
|
||
|
The mirror SHOULD point users to a previous consistent snapshot directory while
|
||
|
it is copying the latest consistent snapshot from PyPI. Only after the latest
|
||
|
consistent snapshot has been completely copied SHOULD the mirror switch clients
|
||
|
to the latest consistent snapshot. The mirror MAY then delete the previous
|
||
|
consistent snapshot once it finds that no client is reading from the previous
|
||
|
consistent snapshot.
|
||
|
|
||
|
The mirror MAY use extant file transfer software such as rsync__ to mirror
|
||
|
PyPI. In that case, the mirror MUST first obtain the latest known timestamp
|
||
|
metadata from PyPI. The mirror MUST NOT immediately publish the latest known
|
||
|
timestamp metadata from PyPI. Instead, the mirror MUST first iteratively
|
||
|
transfer all new files from PyPI until there are no new files left to transfer.
|
||
|
Finally, the mirror MUST publish the latest known timestamp it fetched from
|
||
|
PyPI so that package managers such as pip may be directed to the latest
|
||
|
consistent snapshot known to the mirror.
|
||
|
|
||
|
__ https://rsync.samba.org/
|
||
|
|
||
|
|
||
|
Backup Process
|
||
|
--------------
|
||
|
|
||
|
In order to be able to safely restore from static snapshots later in the event
|
||
|
of a compromise, PyPI SHOULD maintain a small number of its own mirrors to copy
|
||
|
PyPI consistent snapshots according to some schedule. The mirroring protocol
|
||
|
can be used immediately for this purpose. The mirrors must be secured and
|
||
|
isolated such that they are responsible only for mirroring PyPI. The mirrors
|
||
|
can be checked against one another to detect accidental or malicious failures.
|
||
|
|
||
|
|
||
|
Metadata Expiry Times
|
||
|
---------------------
|
||
|
|
||
|
The *root* and *targets* role metadata SHOULD expire in a year, because these
|
||
|
metadata files are expected to change very rarely.
|
||
|
|
||
|
The *claimed* role metadata SHOULD expire in three to six months, because this
|
||
|
metadata is expected to be refreshed in that time frame. This time frame was
|
||
|
chosen to induce an easier administration process for PyPI.
|
||
|
|
||
|
The *timestamp*, *consistent-snapshot*, *recently-claimed* and *unclaimed* role
|
||
|
metadata SHOULD expire in a day because a CDN or mirror SHOULD synchronize
|
||
|
itself with PyPI every day. Furthermore, this generous time frame also takes
|
||
|
into account client clocks that are highly skewed or adrift.
|
||
|
|
||
|
The expiry times for the delegated targets metadata of a project is beyond the
|
||
|
scope of this PEP.
|
||
|
|
||
|
|
||
|
Metadata Scalability
|
||
|
--------------------
|
||
|
|
||
|
Due to the growing number of projects and distributions, the TUF metadata will
|
||
|
also grow correspondingly.
|
||
|
|
||
|
For example, consider the *unclaimed* role. In August 2013, we found that the
|
||
|
size of the *unclaimed* role metadata was about 42MB if the *unclaimed* role
|
||
|
itself signed for about 220K PyPI targets (which are simple indices and
|
||
|
distributions). We will not delve into details in this PEP, but TUF features a
|
||
|
so-called "`lazy bin walk`__" scheme which splits a large targets or delegated
|
||
|
targets metadata file into many small ones. This allows a TUF client updater
|
||
|
to intelligently download only a small number of TUF metadata files in order to
|
||
|
update any project signed for by the *unclaimed* role. For example, applying
|
||
|
this scheme to the previous repository resulted in pip downloading between
|
||
|
1.3KB and 111KB to install or upgrade a PyPI project via TUF.
|
||
|
|
||
|
__ https://github.com/theupdateframework/tuf/issues/39
|
||
|
|
||
|
From our findings as of the time of writing, PyPI SHOULD split all targets in
|
||
|
the *unclaimed* role by delegating it to 1024 delegated targets role, each of
|
||
|
which would sign for PyPI targets whose hashes fall into that "bin" or
|
||
|
delegated targets role. We found that 1024 bins would result in the
|
||
|
*unclaimed* role metadata and each of its binned delegated targets role
|
||
|
metadata to be about the same size (40-50KB) for about 220K PyPI targets
|
||
|
(simple indices and distributions).
|
||
|
|
||
|
It is possible to make the TUF metadata more compact by representing it in a
|
||
|
binary format as opposed to the JSON text format. Nevertheless, we believe
|
||
|
that a sufficiently large number of project and distributions will induce
|
||
|
scalability challenges at some point, and therefore the *unclaimed* role will
|
||
|
then still need delegations in order to address the problem. Furthermore, the
|
||
|
JSON format is an open and well-known standard for data interchange.
|
||
|
|
||
|
Due to the large number of delegated target metadata files, compressed versions
|
||
|
of *consistent-snapshot* metadata SHOULD also be made available.
|
||
|
|
||
|
|
||
|
Key Management
|
||
|
==============
|
||
|
|
||
|
In this section, we examine the kind of keys required to sign for TUF roles on
|
||
|
PyPI. TUF is agnostic with respect to choices of digital signature algorithms.
|
||
|
For the purpose of discussion, we will assume that most digital signatures will
|
||
|
be produced with the well-tested and tried RSA algorithm [20]_. Nevertheless,
|
||
|
we do NOT recommend any particular digital signature algorithm in this PEP
|
||
|
because there are a few important constraints: firstly, cryptography changes
|
||
|
over time; secondly, package managers such as pip may wish to perform signature
|
||
|
verification in Python, without resorting to a compiled C library, in order to
|
||
|
be able to run on as many systems as Python supports; finally, TUF recommends
|
||
|
diversity of keys for certain applications, and we will soon discuss these
|
||
|
exceptions.
|
||
|
|
||
|
|
||
|
Number Of Keys
|
||
|
--------------
|
||
|
|
||
|
The *timestamp*, *consistent-snapshot*, *recently-claimed* and *unclaimed*
|
||
|
roles will need to support continuous delivery. Even though their respective
|
||
|
keys will then need to be online, we will require that the keys be independent
|
||
|
of each other. This allows for each of the keys to be placed on separate
|
||
|
servers if need be, and prevents side channel attacks that compromise one key
|
||
|
from automatically compromising the rest of the keys. Therefore, each of the
|
||
|
*timestamp*, *consistent-snapshot*, *recently-claimed* and *unclaimed* roles
|
||
|
MUST require (1, 1) keys.
|
||
|
|
||
|
The *unclaimed* role MAY delegate targets in an automated manner to a number of
|
||
|
roles called "bins", as we discussed in the previous section. Each of the
|
||
|
"bin" roles SHOULD share the same key as the *unclaimed* role, due
|
||
|
simultaneously to space efficiency of metadata and because there is no security
|
||
|
advantage in requiring separate keys.
|
||
|
|
||
|
The *root* role is critical for security and should very rarely be used. It is
|
||
|
primarily used for key revocation, and it is the root of trust for all of PyPI.
|
||
|
The *root* role signs for the keys that are authorized for each of the
|
||
|
top-level roles (including itself). The keys belonging to the *root* role are
|
||
|
intended to be very well-protected and used with the least frequency of all
|
||
|
keys. We propose that every PSF board member own a (strong) root key. A
|
||
|
majority of them can then constitute the quorum to revoke or endow trust in all
|
||
|
top-level keys. Alternatively, the system administrators of PyPI (instead of
|
||
|
PSF board members) could be responsible for signing for the *root* role.
|
||
|
Therefore, the *root* role SHOULD require (t, n) keys, where n is the number of
|
||
|
either all PyPI administrators or all PSF board members, and t > 1 (so that at
|
||
|
least two members must sign the *root* role).
|
||
|
|
||
|
The *targets* role will be used only to sign for the static delegation of all
|
||
|
targets to the *claimed*, *recently-claimed* and *unclaimed* roles. Since
|
||
|
these target delegations must be secured against attacks in the event of a
|
||
|
compromise, the keys for the *targets* role MUST be offline and independent
|
||
|
from other keys. For simplicity of key management without sacrificing
|
||
|
security, it is RECOMMENDED that the keys of the *targets* role are permanently
|
||
|
discarded as soon as they have been created and used to sign for the role.
|
||
|
Therefore, the *targets* role SHOULD require (1, 1) keys. Again, this is
|
||
|
because the keys are going to be permanently discarded, and more offline keys
|
||
|
will not help against key recovery attacks [21]_ unless diversity of keys is
|
||
|
maintained.
|
||
|
|
||
|
Similarly, the *claimed* role will be used only to sign for the dynamic
|
||
|
delegation of projects to their respective developer keys. Since these target
|
||
|
delegations must be secured against attacks in the event of a compromise, the
|
||
|
keys for the *claimed* role MUST be offline and independent from other keys.
|
||
|
Therefore, the *claimed* role SHOULD require (t, n) keys, where n is the number
|
||
|
of all PyPI administrators (in order to keep it manageable), and t ≥ 1 (so that
|
||
|
at least one member MUST sign the *claimed* role). While a stronger threshold
|
||
|
would indeed render the role more robust against a compromise of the *claimed*
|
||
|
keys (which is highly unlikely assuming that the keys are independent and
|
||
|
securely kept offline), we think that this trade-off is acceptable for the
|
||
|
important purpose of keeping the maintenance overhead for PyPI administrators
|
||
|
as little as possible. At the time of writing, we are keeping this point open
|
||
|
for discussion by the distutils-sig community.
|
||
|
|
||
|
The number of developer keys is project-specific and thus beyond the scope of
|
||
|
this PEP.
|
||
|
|
||
|
|
||
|
Online and Offline Keys
|
||
|
-----------------------
|
||
|
|
||
|
In order to support continuous delivery, the *timestamp*,
|
||
|
*consistent-snapshot*, *recently-claimed* and *unclaimed* role keys MUST be
|
||
|
online.
|
||
|
|
||
|
As explained in the previous section, the *root*, *targets* and *claimed* role
|
||
|
keys MUST be offline for maximum security. Developers keys will be offline in
|
||
|
the sense that the private keys MUST NOT be stored on PyPI, though some of them
|
||
|
may be online on the private infrastructure of the project.
|
||
|
|
||
|
|
||
|
Key Strength
|
||
|
------------
|
||
|
|
||
|
At the time of writing, we recommend that all RSA keys (both offline and
|
||
|
online) SHOULD have a minimum key size of 3072 bits for data-protection
|
||
|
lifetimes beyond 2030 [22]_.
|
||
|
|
||
|
|
||
|
Diversity Of Keys
|
||
|
-----------------
|
||
|
|
||
|
Due to the threats of weak key generation and implementation weaknesses [2]_,
|
||
|
the types of keys as well as the libraries used to generate them should vary
|
||
|
within TUF on PyPI. Our current implementation of TUF supports multiple
|
||
|
digital signature algorithms such as RSA (with OpenSSL [23]_ or PyCrypto [24]_)
|
||
|
and ed25519 [25]_. Furthermore, TUF supports the binding of other
|
||
|
cryptographic libraries that it does not immediately support "out of the box",
|
||
|
and so one MAY generate keys using other cryptographic libraries and use them
|
||
|
for TUF on PyPI.
|
||
|
|
||
|
As such, the root role keys SHOULD be generated by a variety of digital
|
||
|
signature algorithms as implemented by different cryptographic libraries.
|
||
|
|
||
|
|
||
|
Key Compromise Analysis
|
||
|
-----------------------
|
||
|
|
||
|
.. image:: https://raw.github.com/theupdateframework/pep-on-pypi-with-tuf/master/table1.png
|
||
|
|
||
|
Table 1: Attacks possible by compromising certain combinations of role keys
|
||
|
|
||
|
|
||
|
Table 1 summarizes the kinds of attacks rendered possible by compromising a
|
||
|
threshold number of keys belonging to the TUF roles on PyPI. Except for the
|
||
|
*timestamp* and *consistent-snapshot* roles, the pairwise interaction of role
|
||
|
compromises may be found by taking the union of both rows.
|
||
|
|
||
|
In September 2013, we showed how the latest version of pip then was susceptible
|
||
|
to these attacks and how TUF could protect users against them [14]_.
|
||
|
|
||
|
An attacker who compromises developer keys for a project and who is able to
|
||
|
somehow upload malicious metadata and targets to PyPI will be able to serve
|
||
|
malicious updates to users of that project (and that project alone). Note that
|
||
|
compromising *targets* or any delegated targets role (except for project
|
||
|
targets metadata) does not immediately endow the attacker with the ability to
|
||
|
serve malicious updates. The attacker must also compromise the *timestamp* and
|
||
|
*consistent-snapshot* roles (which are both online and therefore more likely to
|
||
|
be compromised). This means that in order to launch any attack, one must be
|
||
|
not only be able to act as a man-in-the-middle but also compromise the
|
||
|
*timestamp* key (or the *root* keys and sign a new *timestamp* key). To launch
|
||
|
any attack other than a freeze attack, one must also compromise the
|
||
|
*consistent-snapshot* key.
|
||
|
|
||
|
Finally, a compromise of the PyPI infrastructure MAY introduce malicious
|
||
|
updates to *recently-claimed* and *unclaimed* projects because the keys for
|
||
|
those roles are online. However, attackers cannot modify *claimed* projects in
|
||
|
such an event because *targets* and *claimed* metadata have been signed with
|
||
|
offline keys. Therefore, it is RECOMMENDED that high-value projects register
|
||
|
their developer keys with PyPI and sign for their own distributions.
|
||
|
|
||
|
|
||
|
In the Event of a Key Compromise
|
||
|
--------------------------------
|
||
|
|
||
|
By a key compromise, we mean that the key as well as PyPI infrastructure has
|
||
|
been compromised and used to sign new metadata on PyPI.
|
||
|
|
||
|
If a threshold number of developer keys of a project have been compromised,
|
||
|
then the project MUST take the following steps:
|
||
|
|
||
|
1. The project metadata and targets MUST be restored to the last known good
|
||
|
consistent snapshot where the project was not known to be compromised. This
|
||
|
can be done by the developers repackaging and resigning all targets with the
|
||
|
new keys.
|
||
|
|
||
|
2. The project delegated targets metadata MUST have their version numbers
|
||
|
incremented, expiry times suitably extended and signatures renewed.
|
||
|
|
||
|
Whereas PyPI MUST take the following steps:
|
||
|
|
||
|
1. Revoke the compromised developer keys from the delegation to the project by
|
||
|
the *recently-claimed* or *claimed* role. This is done by replacing the
|
||
|
compromised developer keys with newly issued developer keys.
|
||
|
|
||
|
2. A new timestamped consistent snapshot MUST be issued.
|
||
|
|
||
|
If a threshold number of *timestamp*, *consistent-snapshot*, *recently-claimed*
|
||
|
or *unclaimed* keys have been compromised, then PyPI MUST take the following
|
||
|
steps:
|
||
|
|
||
|
1. Revoke the *timestamp*, *consistent-snapshot* and *targets* role keys from
|
||
|
the *root* role. This is done by replacing the compromised *timestamp*,
|
||
|
*consistent-snapshot* and *targets* keys with newly issued keys.
|
||
|
|
||
|
2. Revoke the *recently-claimed* and *unclaimed* keys from the *targets* role
|
||
|
by replacing their keys with newly issued keys. Sign the new *targets* role
|
||
|
metadata and discard the new keys (because, as we explained earlier, this
|
||
|
increases the security of *targets* metadata).
|
||
|
|
||
|
3. Clear all targets or delegations in the *recently-claimed* role and delete
|
||
|
all associated delegated targets metadata. Recently registered projects
|
||
|
SHOULD register their developer keys again with PyPI.
|
||
|
|
||
|
4. All targets of the *recently-claimed* and *unclaimed* roles SHOULD be
|
||
|
compared with the last known good consistent snapshot where none of the
|
||
|
*timestamp*, *consistent-snapshot*, *recently-claimed* or *unclaimed* keys
|
||
|
were known to have been compromised. Added, updated or deleted targets in
|
||
|
the compromised consistent snapshot that do not match the last known good
|
||
|
consistent snapshot MAY be restored to their previous versions. After
|
||
|
ensuring the integrity of all *unclaimed* targets, the *unclaimed* metadata
|
||
|
MUST be regenerated.
|
||
|
|
||
|
5. The *recently-claimed* and *unclaimed* metadata MUST have their version
|
||
|
numbers incremented, expiry times suitably extended and signatures renewed.
|
||
|
|
||
|
6. A new timestamped consistent snapshot MUST be issued.
|
||
|
|
||
|
This would preemptively protect all of these roles even though only one of them
|
||
|
may have been compromised.
|
||
|
|
||
|
If a threshold number of the *targets* or *claimed* keys have been compromised,
|
||
|
then there is little that an attacker could do without the *timestamp* and
|
||
|
*consistent-snapshot* keys. In this case, PyPI MUST simply revoke the
|
||
|
compromised *targets* or *claimed* keys by replacing them with new keys in the
|
||
|
*root* and *targets* roles respectively.
|
||
|
|
||
|
If a threshold number of the *timestamp*, *consistent-snapshot* and *claimed*
|
||
|
keys have been compromised, then PyPI MUST take the following steps in addition
|
||
|
to the steps taken when either the *timestamp* or *consistent-snapshot* keys
|
||
|
are compromised:
|
||
|
|
||
|
1. Revoke the *claimed* role keys from the *targets* role and replace them with
|
||
|
newly issued keys.
|
||
|
|
||
|
2. All project targets of the *claimed* roles SHOULD be compared with the last
|
||
|
known good consistent snapshot where none of the *timestamp*,
|
||
|
*consistent-snapshot* or *claimed* keys were known to have been compromised.
|
||
|
Added, updated or deleted targets in the compromised consistent snapshot
|
||
|
that do not match the last known good consistent snapshot MAY be restored to
|
||
|
their previous versions. After ensuring the integrity of all *claimed*
|
||
|
project targets, the *claimed* metadata MUST be regenerated.
|
||
|
|
||
|
3. The *claimed* metadata MUST have their version numbers incremented, expiry
|
||
|
times suitably extended and signatures renewed.
|
||
|
|
||
|
If a threshold number of the *timestamp*, *consistent-snapshot* and *targets*
|
||
|
keys have been compromised, then PyPI MUST take the union of the steps taken
|
||
|
when the *claimed*, *recently-claimed* and *unclaimed* keys have been
|
||
|
compromised.
|
||
|
|
||
|
If a threshold number of the *root* keys have been compromised, then PyPI MUST
|
||
|
take the steps taken when the *targets* role has been compromised as well as
|
||
|
replace all of the *root* keys.
|
||
|
|
||
|
It is also RECOMMENDED that PyPI sufficiently document compromises with
|
||
|
security bulletins. These security bulletins will be most informative when
|
||
|
users of pip with TUF are unable to install or update a project because the
|
||
|
keys for the *timestamp*, *consistent-snapshot* or *root* roles are no longer
|
||
|
valid. They could then visit the PyPI web site to consult security bulletins
|
||
|
that would help to explain why they are no longer able to install or update,
|
||
|
and then take action accordingly. When a threshold number of *root* keys have
|
||
|
not been revoked due to a compromise, then new *root* metadata may be safely
|
||
|
updated because a threshold number of existing *root* keys will be used to sign
|
||
|
for the integrity of the new *root* metadata so that TUF clients will be able
|
||
|
to verify the integrity of the new *root* metadata with a threshold number of
|
||
|
previously known *root* keys. This will be the common case. Otherwise, in the
|
||
|
worst case where a threshold number of *root* keys have been revoked due to a
|
||
|
compromise, an end-user may choose to update new *root* metadata with
|
||
|
`out-of-band`__ mechanisms.
|
||
|
|
||
|
__ https://en.wikipedia.org/wiki/Out-of-band#Authentication
|
||
|
|
||
|
|
||
|
Appendix: Rejected Proposals
|
||
|
============================
|
||
|
|
||
|
|
||
|
Alternative Proposals for Producing Consistent Snapshots
|
||
|
--------------------------------------------------------
|
||
|
|
||
|
The complete file snapshot (CFS) scheme uses file system directories to store
|
||
|
efficient consistent snapshots over time. In this scheme, every consistent
|
||
|
snapshot will be stored in a separate directory, wherein files that are shared
|
||
|
with previous consistent snapshots will be `hard links`__ instead of copies.
|
||
|
|
||
|
__ https://en.wikipedia.org/wiki/Hard_link
|
||
|
|
||
|
The `differential file`__ snapshot (DFS) scheme is a variant of the CFS scheme,
|
||
|
wherein the next consistent snapshot directory will contain only the additions
|
||
|
of new files and updates to existing files of the previous consistent snapshot.
|
||
|
(The first consistent snapshot will contain a complete set of files known
|
||
|
then.) Deleted files will be marked as such in the next consistent snapshot
|
||
|
directory. This means that files will be resolved in this manner: First, set
|
||
|
the current consistent snapshot directory to be the latest consistent snapshot
|
||
|
directory. Then, any requested file will be seeked in the current consistent
|
||
|
snapshot directory. If the file exists in the current consistent snapshot
|
||
|
directory, then that file will be returned. If it has been marked as deleted
|
||
|
in the current consistent snapshot directory, then that file will be reported
|
||
|
as missing. Otherwise, the current consistent snapshot directory will be set
|
||
|
to the preceding consistent snapshot directory and the previous few steps will
|
||
|
be iterated until there is no preceding consistent snapshot to be considered,
|
||
|
at which point the file will be reported as missing.
|
||
|
|
||
|
__ http://dl.acm.org/citation.cfm?id=320484
|
||
|
|
||
|
With the CFS scheme, the trade-off is the I/O costs of producing a consistent
|
||
|
snapshot with the file system. As of October 2013, we found that a fairly
|
||
|
modern computer with a 7200RPM hard disk drive required at least three minutes
|
||
|
to produce a consistent snapshot with the "cp -lr" command on the ext3__ file
|
||
|
system. Perhaps the I/O costs of this scheme may be ameliorated with advanced
|
||
|
tools or file systems such as ZFS__ or btrfs__.
|
||
|
|
||
|
__ https://en.wikipedia.org/wiki/Ext3
|
||
|
__ https://en.wikipedia.org/wiki/ZFS
|
||
|
__ https://en.wikipedia.org/wiki/Btrfs
|
||
|
|
||
|
While the DFS scheme improves upon the CFS scheme in terms of producing faster
|
||
|
consistent snapshots, there are at least two trade-offs. The first is that a
|
||
|
web server will need to be modified to perform the "daisy chain" resolution of
|
||
|
a file. The second is that every now and then, the differential snapshots will
|
||
|
need to be "squashed" or merged together with the first consistent snapshot to
|
||
|
produce a new first consistent snapshot with the latest and complete set of
|
||
|
files. Although the merge cost may be amortized over time, this scheme is not
|
||
|
conceptually si
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
References
|
||
|
==========
|
||
|
|
||
|
.. [1] https://pypi.python.org
|
||
|
.. [2] https://isis.poly.edu/~jcappos/papers/samuel_tuf_ccs_2010.pdf
|
||
|
.. [3] http://www.pip-installer.org
|
||
|
.. [4] https://wiki.python.org/moin/WikiAttack2013
|
||
|
.. [5] https://github.com/theupdateframework/pip/wiki/Attacks-on-software-repositories
|
||
|
.. [6] https://mail.python.org/pipermail/distutils-sig/2013-April/020596.html
|
||
|
.. [7] https://mail.python.org/pipermail/distutils-sig/2013-May/020701.html
|
||
|
.. [8] https://mail.python.org/pipermail/distutils-sig/2013-July/022008.html
|
||
|
.. [9] PEP 381, Mirroring infrastructure for PyPI, Ziadé, Löwis
|
||
|
http://www.python.org/dev/peps/pep-0381/
|
||
|
.. [10] https://mail.python.org/pipermail/distutils-sig/2013-September/022773.html
|
||
|
.. [11] https://mail.python.org/pipermail/distutils-sig/2013-May/020848.html
|
||
|
.. [12] PEP 449, Removal of the PyPI Mirror Auto Discovery and Naming Scheme, Stufft
|
||
|
http://www.python.org/dev/peps/pep-0449/
|
||
|
.. [13] https://isis.poly.edu/~jcappos/papers/cappos_mirror_ccs_08.pdf
|
||
|
.. [14] https://mail.python.org/pipermail/distutils-sig/2013-September/022755.html
|
||
|
.. [15] https://pypi.python.org/security
|
||
|
.. [16] https://github.com/theupdateframework/tuf/blob/develop/docs/tuf-spec.txt
|
||
|
.. [17] PEP 426, Metadata for Python Software Packages 2.0, Coghlan, Holth, Stufft
|
||
|
http://www.python.org/dev/peps/pep-0426/
|
||
|
.. [18] https://en.wikipedia.org/wiki/Continuous_delivery
|
||
|
.. [19] https://mail.python.org/pipermail/distutils-sig/2013-August/022154.html
|
||
|
.. [20] https://en.wikipedia.org/wiki/RSA_%28algorithm%29
|
||
|
.. [21] https://en.wikipedia.org/wiki/Key-recovery_attack
|
||
|
.. [22] http://csrc.nist.gov/publications/nistpubs/800-57/SP800-57-Part1.pdf
|
||
|
.. [23] https://www.openssl.org/
|
||
|
.. [24] https://pypi.python.org/pypi/pycrypto
|
||
|
.. [25] http://ed25519.cr.yp.to/
|
||
|
|
||
|
|
||
|
Acknowledgements
|
||
|
================
|
||
|
|
||
|
Nick Coghlan, Daniel Holth and the distutils-sig community in general for
|
||
|
helping us to think about how to usably and efficiently integrate TUF with
|
||
|
PyPI.
|
||
|
|
||
|
Roger Dingledine, Sebastian Hahn, Nick Mathewson, Martin Peck and Justin
|
||
|
Samuel for helping us to design TUF from its predecessor Thandy of the Tor
|
||
|
project.
|
||
|
|
||
|
Konstantin Andrianov, Geremy Condra, Vladimir Diaz, Zane Fisher, Justin Samuel,
|
||
|
Tian Tian, Santiago Torres, John Ward, and Yuyu Zheng for helping us to develop
|
||
|
TUF.
|
||
|
|
||
|
Vladimir Diaz, Monzur Muhammad and Sai Teja Peddinti for helping us to review
|
||
|
this PEP.
|
||
|
|
||
|
Zane Fisher for helping us to review and transcribe this PEP.
|
||
|
|
||
|
|
||
|
Copyright
|
||
|
=========
|
||
|
|
||
|
This document has been placed in the public domain.
|