-
Notifications
You must be signed in to change notification settings - Fork 2
PyPI compromise and pip without TUF
In the event of a PyPI compromise, we want to see how pip is affected. When and how would an attacker be able to coax pip into install malicious packages?
First, we set up the virtual environment (for cleanroom testing) and install pip-without-TUF:
$ cd /tmp
$ curl -O https://pypi.python.org/packages/source/v/virtualenv/virtualenv-1.10.1.tar.gz
$ tar xvfz virtualenv-1.10.1.tar.gz
$ python virtualenv-1.10.1/virtualenv.py --no-site-packages pypi-compromise-without-tuf
$ source pypi-compromise-without-tuf/bin/activate
First, suppose that we introduce the FooBar package on PyPI-with-TUF by having the "unclaimed" targets sign for it. This means that the developers of FooBar have not yet "claimed" ownership (and thus responsibility) for the package, so PyPI will use an online key to sign for all "unclaimed" targets. (This key is online to allow for continuous release of "unclaimed" PyPI targets.) This is the lowest level of security for a package on PyPI-with-TUF: we will protect the package from being tampered by a mirror or a CDN, but we cannot protect it from being tampered by an attacker who has compromised PyPI.
$ pip install FooBar --index-url http://mirror1.poly.edu/test-pip/pypi-compromise/repository.unclaimed.good/targets/simple/
Downloading/unpacking FooBar
Downloading FooBar-0.1.tar.gz
Running setup.py egg_info for package FooBar
Installing collected packages: FooBar
Running setup.py install for FooBar
FooBar 0.1
Successfully installed FooBar
Cleaning up...
Let us see what happens when an attacker compromises PyPI and tampers with the FooBar package targets and signs for them with the online "unclaimed" targets role key:
$ pip install --upgrade FooBar --index-url http://mirror1.poly.edu/test-pip/pypi-compromise/repository.unclaimed.bad/targets/simple/
Downloading/unpacking FooBar from http://mirror1.poly.edu/test-pip/pypi-compromise/repository.unclaimed.bad/targets/packages/source/F/FooBar/FooBar-0.2.tar.gz#md5=10831baad99c6acbcd59103b1099d13c
Downloading FooBar-0.2.tar.gz
Running setup.py egg_info for package FooBar
Installing collected packages: FooBar
Found existing installation: FooBar 0.1
Uninstalling FooBar:
Successfully uninstalled FooBar
Running setup.py install for FooBar
TAMPERED FooBar 0.2
Successfully installed FooBar
Cleaning up...
In summary, the "unclaimed" targets role will allow for continuous release of PyPI packages, as is the case of PyPI-without-TUF today. However, unlike PyPI-without-TUF, it will protect all "unclaimed" PyPI packages from attacks by mirrors or CDNs. (An "unclaimed" PyPI package is one for which its developers have not registered their own signing keys to sign for all TUF metadata about that package.) Nevertheless, since the "unclaimed" targets role key is online to allow continuous release, any compromise of PyPI will not protect "unclaimed" targets. (We will soon see how to protect PyPI packages even against a compromise of PyPI with the "claimed" targets role.)
Now, suppose that developers of the FooBar package register their signing keys (to sign for all FooBar targets) with PyPI-with-TUF. PyPI-with-TUF will then delegate all FooBar targets from the "recently-claimed" targets role to the FooBar targets role. If a target is available in both the "recently-claimed" and "unclaimed" targets roles, "recently-claimed" would be preferred over "unclaimed" to be responsible for the target:
$ pip install --upgrade FooBar --index-url http://mirror1.poly.edu/test-pip/pypi-compromise/repository.recently-claimed.good/targets/simple/
Downloading/unpacking FooBar from http://mirror1.poly.edu/test-pip/pypi-compromise/repository.recently-claimed.good/targets/packages/source/F/FooBar/FooBar-0.3.tar.gz#md5=eb534887d5d531c594715bd929c6c789
Downloading FooBar-0.3.tar.gz
Running setup.py egg_info for package FooBar
Installing collected packages: FooBar
Found existing installation: FooBar 0.2
Uninstalling FooBar:
Successfully uninstalled FooBar
Running setup.py install for FooBar
FooBar 0.3
Successfully installed FooBar
Cleaning up...
However, it is important to note that "recently-claimed" does not offer less or more security than "unclaimed". "recently-claimed" allows for the continuous release of PyPI packages for which their respective developers have registered their own package-signing keys; therefore, the signing key for "recently-claimed", too, must remain online. Here is an example of an attacker who has compromised PyPI and modifies the delegation of the FooBar package in "recently-claimed":
$ pip install --upgrade FooBar --index-url http://mirror1.poly.edu/test-pip/pypi-compromise/repository.recently-claimed.bad/targets/simple/
Downloading/unpacking FooBar from http://mirror1.poly.edu/test-pip/pypi-compromise/repository.recently-claimed.bad/targets/packages/source/F/FooBar/FooBar-0.4.tar.gz#md5=5d21b9feebf2118d3e0916cbfce22176
Downloading FooBar-0.4.tar.gz
Running setup.py egg_info for package FooBar
Installing collected packages: FooBar
Found existing installation: FooBar 0.3
Uninstalling FooBar:
Successfully uninstalled FooBar
Running setup.py install for FooBar
TAMPERED FooBar 0.4
Successfully installed FooBar
Cleaning up...
What is the point of "recently-claimed", then? It exists so that PyPI-with-TUF may continuously release packages for which developers have registered their own package-signing keys. Once in a while, PyPI-with-TUF will strongly secure the delegations in "recently-claimed" by promoting them to the "claimed" targets role.
Suppose that PyPI-with-TUF has promoted the FooBar package from "recently-claimed" to "claimed". If a target is available in the "claimed", "recently-claimed", and "unclaimed" targets roles, then "claimed" would be the most preferred role to be responsible for the target:
$ pip install --upgrade FooBar --index-url http://mirror1.poly.edu/test-pip/pypi-compromise/repository.claimed.good/targets/simple/
Downloading/unpacking FooBar from http://mirror1.poly.edu/test-pip/pypi-compromise/repository.claimed.good/targets/packages/source/F/FooBar/FooBar-0.5.tar.gz#md5=02ccbb78795c3671b7f3b856ec9a6951
Downloading FooBar-0.5.tar.gz
Running setup.py egg_info for package FooBar
Installing collected packages: FooBar
Found existing installation: FooBar 0.4
Uninstalling FooBar:
Successfully uninstalled FooBar
Running setup.py install for FooBar
FooBar 0.5
Successfully installed FooBar
Cleaning up...
In this case, the pip-with-TUF user trusted PyPI to provide secure metadata about which package-signing keys the developers of the FooBar package intended for the package. This delegation metadata is secure because the "claimed" targets role key is offline.
Suppose that an attacker compromised PyPI and tampered with the TUF metadata for the FooBar package:
$ pip install --upgrade FooBar --index-url http://mirror1.poly.edu/test-pip/pypi-compromise/repository.claimed.bad/targets/simple/
Downloading/unpacking FooBar from http://mirror1.poly.edu/test-pip/pypi-compromise/repository.claimed.bad/targets/packages/source/F/FooBar/FooBar-0.6.tar.gz#md5=de819178b0c898756b6d1cc91bb24546
Downloading FooBar-0.6.tar.gz
Running setup.py egg_info for package FooBar
Installing collected packages: FooBar
Found existing installation: FooBar 0.5
Uninstalling FooBar:
Successfully uninstalled FooBar
Running setup.py install for FooBar
TAMPERED FooBar 0.6
Successfully installed FooBar
Cleaning up...
Unfortunately, this user has now been compromised with a malicious FooBar package. This situation could have been avoided with the resilience to compromise offered by PyPI-with-TUF.