This repository has been archived by the owner on Jan 12, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 117
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #400 from dephell/package-validate
dephell package verify
- Loading branch information
Showing
7 changed files
with
174 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
# built-in | ||
from argparse import ArgumentParser | ||
from pathlib import Path | ||
from tempfile import TemporaryDirectory | ||
|
||
# app | ||
from ..actions import get_package, make_json | ||
from ..config import builders | ||
from ..imports import lazy_import | ||
from ..networking import requests_session | ||
from .base import BaseCommand | ||
|
||
|
||
gnupg = lazy_import('gnupg', package='python-gnupg') | ||
DEFAULT_KEYSERVER = 'pgp.mit.edu' | ||
|
||
|
||
class PackageVerifyCommand(BaseCommand): | ||
"""Verify GPG signature for a release from PyPI.org. | ||
""" | ||
@staticmethod | ||
def build_parser(parser) -> ArgumentParser: | ||
builders.build_config(parser) | ||
builders.build_output(parser) | ||
builders.build_api(parser) | ||
builders.build_other(parser) | ||
parser.add_argument('name', help='package name and version to validate') | ||
return parser | ||
|
||
def __call__(self) -> bool: | ||
dep = get_package(self.args.name, repo=self.config.get('repo')) | ||
releases = dep.repo.get_releases(dep) | ||
if not releases: | ||
self.logger.error('cannot find releases for the package') | ||
return False | ||
releases = dep.constraint.filter(releases=releases) | ||
if not releases: | ||
self.logger.error('cannot find releases for the constraint') | ||
return False | ||
release = sorted(releases, reverse=True)[0] | ||
gpg = gnupg.GPG() | ||
return self._verify_release(release=release, gpg=gpg) | ||
|
||
def _verify_release(self, release, gpg) -> bool: | ||
if not release.urls: | ||
self.logger.error('no urls found for release', extra=dict( | ||
version=str(release.version), | ||
)) | ||
return False | ||
|
||
verified = False | ||
all_valid = True | ||
with TemporaryDirectory() as root_path: | ||
for url in release.urls: | ||
sign_path = Path(root_path) / 'archive.bin.asc' | ||
with requests_session() as session: | ||
response = session.get(url + '.asc') | ||
if response.status_code == 404: | ||
self.logger.debug('no signature found', extra=dict(url=url)) | ||
continue | ||
sign_path.write_bytes(response.content) | ||
|
||
self.logger.info('getting release file...', extra=dict(url=url)) | ||
with requests_session() as session: | ||
response = session.get(url) | ||
if response.status_code == 404: | ||
self.logger.debug('no signature found', extra=dict(url=url)) | ||
continue | ||
data = response.content | ||
|
||
info = self._verify_data(gpg=gpg, sign_path=sign_path, data=data) | ||
verified = True | ||
if not info: | ||
return False | ||
if info['status'] != 'signature valid': | ||
all_valid = False | ||
info['release'] = str(release.version) | ||
info['name'] = url.rsplit('/', maxsplit=1)[-1] | ||
print(make_json( | ||
data=info, | ||
key=self.config.get('filter'), | ||
colors=not self.config['nocolors'], | ||
table=self.config['table'], | ||
)) | ||
if not verified: | ||
self.logger.error('no signed files found') | ||
return False | ||
return all_valid | ||
|
||
def _verify_data(self, gpg, sign_path: Path, data: bytes, retry: bool = True): | ||
verif = gpg.verify_data(str(sign_path), data) | ||
result = dict( | ||
created=verif.creation_date, | ||
fingerprint=verif.fingerprint, | ||
key_id=verif.key_id, | ||
status=verif.status, | ||
username=verif.username, | ||
) | ||
|
||
if verif.status == 'no public key' and retry: | ||
# try to import keys and verify again | ||
self.logger.debug('searching the key...', extra=dict(key_id=verif.key_id)) | ||
keys = gpg.search_keys(query=verif.key_id, keyserver=DEFAULT_KEYSERVER) | ||
if len(keys) != 1: | ||
self.logger.debug('cannot find the key', extra=dict( | ||
count=len(keys), | ||
key_id=verif.key_id, | ||
)) | ||
return result | ||
gpg.recv_keys(DEFAULT_KEYSERVER, keys[0]['keyid']) | ||
return self._verify_data(gpg=gpg, sign_path=sign_path, data=data, retry=False) | ||
|
||
return result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# dephell package verify | ||
|
||
Verify GPG signature for a release from [PyPI](https://pypi.org/). | ||
|
||
Verify files for the latest release: | ||
|
||
```bash | ||
$ dephell package verify flask | ||
INFO getting release file... (url=https://files.pythonhosted.org/packages/.../Flask-1.1.1-py2.py3-none-any.whl) | ||
{ | ||
"created": "2019-07-08", | ||
"fingerprint": "AD253D8661D175D001F462D77A1C87E3F5BC42A8", | ||
"key_id": "7A1C87E3F5BC42A8", | ||
"name": "Flask-1.1.1-py2.py3-none-any.whl", | ||
"release": "1.1.1", | ||
"status": "signature valid", | ||
"username": "David Lord <davidism@gmail.com>" | ||
} | ||
|
||
INFO getting release file... (url=https://files.pythonhosted.org/packages/.../Flask-1.1.1.tar.gz) | ||
{ | ||
"created": "2019-07-08", | ||
"fingerprint": "AD253D8661D175D001F462D77A1C87E3F5BC42A8", | ||
"key_id": "7A1C87E3F5BC42A8", | ||
"name": "Flask-1.1.1.tar.gz", | ||
"release": "1.1.1", | ||
"status": "signature valid", | ||
"username": "David Lord <davidism@gmail.com>" | ||
} | ||
``` | ||
|
||
Verify files for the given release: | ||
|
||
```bash | ||
dephell package verify django==2.0.1 | ||
``` | ||
|
||
Note that packages signing isn't popular in Python world. Most of packages have no signature: | ||
|
||
```bash | ||
$ dephell package verify pip | ||
ERROR no signed files found | ||
``` | ||
|
||
## See also | ||
|
||
1. [How to filter commands JSON output](filters). | ||
1. [dephell package show](cmd-package-show) to show information about single package. | ||
1. [dephell deps audit](cmd-deps-audit) to find known vulnerabilities in the project dependencies. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters