Commits

Daniel Holth  committed 212b57d

remember signing keys

  • Participants
  • Parent commits 47ee18a

Comments (0)

Files changed (6)

 docs/_build
 htmlcov
 requirements.txt
+__pycache__

File wheel/__main__.py

 """
-Wheel command-line utility.
+Wheel command line tool (enable python -m wheel syntax)
 """
 
-import os
-import baker
-import ed25519ll
-import hashlib
-import sys
-import wheel.install
-import wheel.signatures
-import json
-from .util import urlsafe_b64decode, urlsafe_b64encode, native, binary
-
-wb = baker.Baker()
-
-@wb.command
-def keygen():
-    """Generate a public/private key pair."""
-    import keyring
-    keypair = ed25519ll.crypto_sign_keypair()
-    vk = binary(urlsafe_b64encode(keypair.vk))
-    sk = binary(urlsafe_b64encode(keypair.sk))
-    kr = keyring.get_keyring()
-    kr.set_password("wheel", vk, sk)
-    sys.stdout.write("Created Ed25519 keypair with vk={0}\n".format(vk))
-    if isinstance(kr, keyring.backend.BasicFileKeyring):
-        sys.stdout.write("in {0}\n".format(kr.file_path))
-    else:
-        sys.stdout.write("in %r\n" % kr)
-
-    sk2 = kr.get_password('wheel', vk)
-    if sk2 != sk:
-        raise Exception("Keyring is broken. Could not retrieve secret key.")
-
-@wb.command
-def sign(wheelfile, replace=False):
-    """Sign a wheel"""    
-    wf = wheel.install.WheelFile(wheelfile, append=True)
-    record_name = wf.distinfo_name + '/RECORD'
-    sig_name = wf.distinfo_name + '/RECORD.jws'
-    if sig_name in wf.zipfile.namelist(): 
-        raise NotImplementedError("Wheel is already signed")
-    record_data = wf.zipfile.read(record_name)
-    payload = {"hash":"sha256="+native(urlsafe_b64encode(hashlib.sha256(record_data).digest()))}
-    sig = wheel.signatures.sign(payload, ed25519ll.crypto_sign_keypair())
-    wf.zipfile.writestr(sig_name, json.dumps(sig, sort_keys=True))
-    wf.zipfile.close()
-
-@wb.command
-def verify(wheelfile):
-    """Verify a wheel."""
-    import pprint
-    wf = wheel.install.WheelFile(wheelfile)
-    sig_name = wf.distinfo_name + '/RECORD.jws'
-    sig = json.loads(native(wf.zipfile.open(sig_name).read()))
-    sys.stdout.write("Signatures are internally consistent.\n%s\n" % (
-                     pprint.pformat(wheel.signatures.verify(sig),)))
-
-@wb.command(shortopts={'dest': 'd'})
-def unpack(wheelfile, dest='.'):
-    """Unpack a wheel.
-
-    Wheel content will be unpacked to {dest}/{name}-{ver}, where {name}
-    is the package name and {ver} its version.
-
-    :param wheelfile: The path to the wheel.
-    :param dest: Destination directory (default to current directory).
-    """
-    wf = wheel.install.WheelFile(wheelfile)
-    namever = wf.parsed_filename.group('namever')
-    destination = os.path.join(dest, namever)
-    sys.stdout.write("Unpacking to: %s\n" % (destination))
-    wf.zipfile.extractall(destination)
-    wf.zipfile.close()
-
-
 def main(): # needed for console script
-    wb.run()
+    import wheel.tool
+    wheel.tool.main()
 
 if __name__ == "__main__":
     main()

File wheel/bdist_wheel.py

 from pkg_resources import safe_name, safe_version
 
 from shutil import rmtree
-from email.parser import Parser
 from email.generator import Generator
 
 from distutils.util import get_platform

File wheel/install.py

 import os.path
 import re
 import zipfile
-import hmac
 import hashlib
 import csv
 from email.parser import Parser
 
 from wheel.decorator import reify
-from wheel.util import urlsafe_b64encode, utf8, to_json, from_json,\
+from wheel.util import urlsafe_b64encode, from_json,\
     urlsafe_b64decode, native, binary
 from wheel import signatures
 
 
     @property
     def arity(self):
-        '''The number of compatibility tags the wheel is compatible with.'''
+        '''The number of compatibility tags the wheel declares.'''
         return len(list(self.compatibility_tags))
 
     def compatibility_rank(self, supported):
             raise ValueError("Wheel version is too high")
         
     def verify(self, zipfile=None):
-        """Verify the VerifyingZipFile `zipfile` by verifying its signature 
+        """Configure the VerifyingZipFile `zipfile` by verifying its signature 
         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)."""
         return min(ranked)
     return sorted(ranked)
 
-
-def install(wheel_path):
-    """Install a single wheel (.whl) file without regard for dependencies."""
-    try:
-        sys.real_prefix
-    except AttributeError:
-        raise Exception(
-            "This alpha version of wheel will only install into a virtualenv")
-    wf = WheelFile(wheel_path)
-    raise NotImplementedError()

File wheel/keys.py

+"""Store and retrieve wheel signing / verifying keys.
+
+Given a scope (a package name, + meaning "all packages", or - meaning 
+"no packages"), return a list of verifying keys that are trusted for that 
+scope.
+
+Given a package name, return a list of (scope, key) suggested keys to sign
+that package (only the verifying keys; the private signing key is stored
+elsewhere).
+
+Keys here are represented as urlsafe_b64encoded strings with no padding.
+
+Tentative command line interface:
+
+# list trusts
+wheel trust
+# trust a particular key for all
+wheel trust + key
+# trust key for beaglevote
+wheel trust beaglevote key
+# stop trusting a key for all
+wheel untrust + key
+
+# generate a key pair
+wheel keygen
+
+# import a signing key from a file
+wheel import keyfile
+
+# export a signing key
+wheel export key
+"""
+
+import json
+import dirspec.basedir
+import os.path
+from wheel.util import urlsafe_b64encode, urlsafe_b64decode, native, binary
+
+class WheelKeys(object):
+    def __init__(self):
+        self.data = {'signers':[], 'verifiers':[]}
+        
+    def load(self):
+        # XXX JSON is not a great database
+        for path in dirspec.basedir.load_config_paths('wheel'):
+            conf = os.path.join(path, 'wheel.json')
+            if os.path.exists(conf):
+                with open(conf, 'r') as infile:
+                    self.data = json.load(infile)
+                    for x in ('signers', 'verifiers'):
+                        if not x in self.data:
+                            self.data[x] = []
+                break
+        return self
+
+    def save(self):
+        # Try not to call this a very long time after load() 
+        path = dirspec.basedir.save_config_path('wheel')
+        conf = os.path.join(path, 'wheel.json')
+        with open(conf, 'w+') as out:
+            json.dump(self.data, out, indent=2)
+        return self
+    
+    def trust(self, scope, vk):
+        """Start trusting a particular key for given scope."""
+        self.data['verifiers'].append({'scope':scope, 'vk':vk})
+        return self
+    
+    def untrust(self, scope, vk):
+        """Stop trusting a particular key for given scope."""
+        self.data['verifiers'].remove({'scope':scope, 'vk':vk})
+        return self
+        
+    def trusted(self, scope=None):
+        """Return list of [(scope, trusted key), ...] for given scope."""
+        trust = [(x['scope'], x['vk']) for x in self.data['verifiers'] if x['scope'] in (scope, '+')]
+        trust.sort() # hack '+' will usually sort before all valid package names
+        trust.reverse()
+        return trust
+    
+    def signers(self, scope):
+        """Return list of signing key(s)."""
+        sign = [(x['scope'], x['vk']) for x in self.data['verifiers'] if x['scope'] in (scope, '+')]
+        sign.sort() # XXX ONLY SORT BY SCOPE (most recently added signer should win)
+        sign.reverse()
+        return sign
+    
+    def add_signer(self, scope, vk):
+        """Remember verifying key vk as being valid for signing in scope."""
+        self.data['signers'].append({'scope':scope, 'vk':vk})
+    

File wheel/tool/__init__.py

+"""
+Wheel command-line utility.
+"""
+
+import os
+import baker
+import hashlib
+import sys
+import wheel.install
+import wheel.signatures
+import wheel.keys
+import json
+from ..util import urlsafe_b64decode, urlsafe_b64encode, native, binary
+
+wb = baker.Baker()
+
+@wb.command
+def keygen():
+    """Generate a public/private key pair."""
+    import keyring
+    import ed25519ll
+
+    wk = wheel.keys.WheelKeys().load()
+    
+    keypair = ed25519ll.crypto_sign_keypair()
+    vk = binary(urlsafe_b64encode(keypair.vk))
+    sk = binary(urlsafe_b64encode(keypair.sk))
+    kr = keyring.get_keyring()
+    kr.set_password("wheel", vk, sk)
+    sys.stdout.write("Created Ed25519 keypair with vk={0}\n".format(vk))
+    if isinstance(kr, keyring.backend.BasicFileKeyring):
+        sys.stdout.write("in {0}\n".format(kr.file_path))
+    else:
+        sys.stdout.write("in %r\n" % kr.__class__)
+
+    sk2 = kr.get_password('wheel', vk)
+    if sk2 != sk:
+        raise Exception("Keyring is broken. Could not retrieve secret key.")
+    
+    sys.stdout.write("Trusting {0} to sign and verify all packages.\n".format(vk))
+    wk.add_signer('+', vk)
+    wk.trust('+', vk)
+    wk.save()
+
+@wb.command
+def sign(wheelfile, replace=False):
+    """Sign a wheel"""
+    import keyring
+    import ed25519ll
+    
+    wf = wheel.install.WheelFile(wheelfile, append=True)
+    wk = wheel.keys.WheelKeys().load()
+    
+    name = wf.parsed_filename.group('name')
+    sign_with = wk.signers(name)[0]
+    vk = binary(sign_with[1])
+    sys.stdout.write("Signing {0} with {1}\n".format(name, sign_with[1]))
+    
+    sk = binary(keyring.get_keyring().get_password('wheel', vk))
+    keypair = ed25519ll.Keypair(urlsafe_b64decode(vk), urlsafe_b64decode(sk))
+    
+    record_name = wf.distinfo_name + '/RECORD'
+    sig_name = wf.distinfo_name + '/RECORD.jws'
+    if sig_name in wf.zipfile.namelist(): 
+        raise NotImplementedError("Wheel is already signed")
+    record_data = wf.zipfile.read(record_name)
+    payload = {"hash":"sha256="+native(urlsafe_b64encode(hashlib.sha256(record_data).digest()))}
+    sig = wheel.signatures.sign(payload, keypair)
+    wf.zipfile.writestr(sig_name, json.dumps(sig, sort_keys=True))
+    wf.zipfile.close()
+
+@wb.command
+def verify(wheelfile):
+    """Verify a wheel."""
+    import pprint
+    wf = wheel.install.WheelFile(wheelfile)
+    sig_name = wf.distinfo_name + '/RECORD.jws'
+    sig = json.loads(native(wf.zipfile.open(sig_name).read()))
+    sys.stdout.write("Signatures are internally consistent.\n%s\n" % (
+                     pprint.pformat(wheel.signatures.verify(sig),)))
+
+@wb.command(shortopts={'dest': 'd'})
+def unpack(wheelfile, dest='.'):
+    """Unpack a wheel.
+
+    Wheel content will be unpacked to {dest}/{name}-{ver}, where {name}
+    is the package name and {ver} its version.
+
+    :param wheelfile: The path to the wheel.
+    :param dest: Destination directory (default to current directory).
+    """
+    wf = wheel.install.WheelFile(wheelfile)
+    namever = wf.parsed_filename.group('namever')
+    destination = os.path.join(dest, namever)
+    sys.stdout.write("Unpacking to: %s\n" % (destination))
+    wf.zipfile.extractall(destination)
+    wf.zipfile.close()
+
+def main():
+    wb.run()
+