Daniel Holth avatar Daniel Holth committed e2d30b2

binary pkginfo read/write on Py3; version 0.9

Comments (0)

Files changed (8)

+0.9
+===
+- Updated digital signatures scheme
+- Python 3 support for digital signatures
+- Always verify RECORD hashes on extract
+- "wheel" command line tool to sign, verify, unpack wheel files
+
 0.8
 ===
 - none/any draft pep tags update
 CHANGES = codecs.open(os.path.join(here, 'CHANGES.txt'), encoding='utf8').read()
 
 setup(name='wheel',
-      version='0.8',
+      version='0.9',
       description='A built-package format for Python.',
       long_description=README + '\n\n' +  CHANGES,
       classifiers=[

wheel/__main__.py

 import ed25519ll
 import hashlib
 import sys
-import keyring
 import wheel.install
 import wheel.signatures
 import json
 @wb.command
 def keygen():
     """Generate a public/private key pair."""
+    import keyring
     keypair = ed25519ll.crypto_sign_keypair()
     vk = urlsafe_b64encode(keypair.vk).decode('latin1')
     sk = urlsafe_b64encode(keypair.sk).decode('latin1')

wheel/bdist_wheel.py

 from distutils import log as logger
 import shutil
 
-from wheel.util import get_abbr_impl, get_impl_ver
-from wheel.archive import archive_wheelfile
+from .util import get_abbr_impl, get_impl_ver
+from .archive import archive_wheelfile
+from .pkginfo import read_pkg_info, write_pkg_info
 
 def open_for_csv(name, mode):
     if sys.version_info[0] < 3:
         # XXX does Requires: become Requires-Dist: ?
         # (very few source packages include Requires: (644) or
         # Requires-Dist: (5) in PKG-INFO); packaging treats both identically
-        pkg_info = Parser().parse(open(pkginfo_path, 'r'))
+        pkg_info = read_pkg_info(pkginfo_path)
         pkg_info.replace_header('Metadata-Version', '1.2')
         requires_path = os.path.join(egg_info_path, 'requires.txt')
         if os.path.exists(requires_path):
             if not open(dependency_links, 'r').read().strip(): 
                 adios(dependency_links)
 
-        with open(os.path.join(distinfo_path, 'METADATA'), 'w') as metadata:
-            Generator(metadata, maxheaderlen=0).flatten(pkg_info)
+        write_pkg_info(os.path.join(distinfo_path, 'METADATA'), pkg_info)
 
         adios(egginfo_path)
 
 
 from wheel.decorator import reify
 from wheel.util import urlsafe_b64encode, utf8, to_json, from_json,\
-    urlsafe_b64decode
+    urlsafe_b64decode, native, binary
 from wheel import signatures
 
 # The next major version after this version of the 'wheel' tool:
         and setting expected hashes for every hash in RECORD.
         Caller must complete the verification process by completely reading 
         every file in the archive (e.g. with extractall)."""
+        sig = None
         if zipfile is None:
             zipfile = self.zipfile
         zipfile.strict = True
         record = zipfile.read(record_name)
                 
         record_digest = urlsafe_b64encode(hashlib.sha256(record).digest())
-        sig = from_json(zipfile.read(sig_name))
-        headers, payload = signatures.verify(sig)
-        if payload['hash'] != "sha256=" + record_digest:
-            raise BadWheelFile("Claimed RECORD hash != computed hash.")
+        try:
+            sig = from_json(zipfile.read(sig_name))
+        except KeyError: # no signature
+            pass
+        if sig:
+            headers, payload = signatures.verify(sig)
+            if payload['hash'] != "sha256=" + record_digest:
+                raise BadWheelFile("Claimed RECORD hash != computed hash.")
         
-        reader = csv.reader(record.splitlines())
+        reader = csv.reader((native(r) for r in record.splitlines()))
         
         for row in reader:
             filename = row[0]
                 continue
             algo, data = row[1].split('=', 1)
             assert algo == "sha256", "Unsupported hash algorithm"
-            zipfile.set_expected_hash(filename, urlsafe_b64decode(data))
+            zipfile.set_expected_hash(filename, urlsafe_b64decode(binary(data)))
     
     
 class VerifyingZipFile(zipfile.ZipFile):
+"""Tools for reading and writing PKG-INFO / METADATA without caring
+about the encoding."""
+
+from email.parser import Parser
+
+try:
+    unicode
+    _PY3 = False
+except NameError:
+    _PY3 = True
+
+if not _PY3:
+    from email.generator import Generator
+    
+    def read_pkg_info(path):
+        with open(path, "r") as headers:
+            message = Parser().parse(headers)
+        return message
+
+    def write_pkg_info(path, message):
+        with open(path, 'w') as metadata:
+            Generator(metadata, maxheaderlen=0).flatten(message) 
+
+else:
+    from email.generator import BytesGenerator
+    def read_pkg_info(path):
+        with open(path, "r", 
+                  encoding="ascii", 
+                  errors="surrogateescape") as headers:
+            message = Parser().parse(headers)
+        return message
+
+    def write_pkg_info(path, message):
+        with open(path, "wb") as out:
+            BytesGenerator(out, maxheaderlen=0).flatten(message)
+

wheel/signatures.py

     import ed25519ll
 except ImportError:
     ed25519ll = None
-from wheel.util import urlsafe_b64decode, urlsafe_b64encode
+from wheel.util import urlsafe_b64decode, urlsafe_b64encode, native, binary
 
-try:
-    unicode
-    def native(s):
-        return s
-    def binary(s):
-        if isinstance(s, unicode):
-            return s.encode('latin1')
-        return s
-except NameError:
-    def native(s):
-        if isinstance(s, bytes):
-            return s.decode('latin1')
-        return s
-    def binary(s):
-        if isinstance(s, str):
-            return s.encode('latin1')
 
 def sign(payload, keypair):
     """Return a JWS-JS format signature given a JSON-serializable payload and 
         return data
 
 
+try:
+    unicode
+    def native(s):
+        return s
+    def binary(s):
+        if isinstance(s, unicode):
+            return s.encode('latin1')
+        return s
+except NameError:
+    def native(s):
+        if isinstance(s, bytes):
+            return s.decode('latin1')
+        return s
+    def binary(s):
+        if isinstance(s, str):
+            return s.encode('latin1')
+
+
 def get_abbr_impl():
     """Return abbreviated implementation name."""
     if hasattr(sys, 'pypy_version_info'):
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.