Ticket #5894: 5894-ckeygen.diff

File 5894-ckeygen.diff, 21.8 KB (added by Lucas Taylor, 9 years ago)

Fix ckeygen --changepass; add tests

  • twisted/conch/test/test_keys.py

     
    309309SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7
    310310CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE
    311311xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P
    312 -----END RSA PRIVATE KEY-----""")
     312-----END RSA PRIVATE KEY-----""", passphrase='encrypted')
    313313        # key with invalid encryption type
    314314        self.assertRaises(
    315315            keys.BadKeyError, keys.Key.fromString,
     
    342342SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7
    343343CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE
    344344xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P
    345 -----END RSA PRIVATE KEY-----""")
     345-----END RSA PRIVATE KEY-----""", passphrase='encrypted')
    346346        # key with bad IV (AES)
    347347        self.assertRaises(
    348348            keys.BadKeyError, keys.Key.fromString,
     
    375375SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7
    376376CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE
    377377xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P
    378 -----END RSA PRIVATE KEY-----""")
     378-----END RSA PRIVATE KEY-----""", passphrase='encrypted')
    379379        # key with bad IV (DES3)
    380380        self.assertRaises(
    381381            keys.BadKeyError, keys.Key.fromString,
     
    408408SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7
    409409CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE
    410410xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P
    411 -----END RSA PRIVATE KEY-----""")
     411-----END RSA PRIVATE KEY-----""", passphrase='encrypted')
    412412
    413413    def test_fromFile(self):
    414414        """
  • twisted/conch/test/test_ckeygen.py

     
    55Tests for L{twisted.conch.scripts.ckeygen}.
    66"""
    77
     8import getpass
    89import sys
    910from StringIO import StringIO
    1011
     
    1415except ImportError:
    1516    skip = "PyCrypto and pyasn1 required for twisted.conch.scripts.ckeygen."
    1617else:
    17     from twisted.conch.ssh.keys import Key
    18     from twisted.conch.scripts.ckeygen import printFingerprint, _saveKey
     18    from twisted.conch.ssh.keys import Key, BadKeyError
     19    from twisted.conch.scripts.ckeygen import (
     20        changePassPhrase, displayPublicKey, printFingerprint, _saveKey
     21    )
    1922
    2023from twisted.python.filepath import FilePath
    2124from twisted.trial.unittest import TestCase
    22 from twisted.conch.test.keydata import publicRSA_openssh, privateRSA_openssh
     25from twisted.conch.test.keydata import (
     26    publicRSA_openssh, privateRSA_openssh, privateRSA_openssh_encrypted
     27)
    2328
    2429
    2530
     
    7883            Key.fromString(base.child('id_rsa.pub').getContent()),
    7984            key.public())
    8085
     86
     87    def test_displayPublicKey(self):
     88        """
     89        L{displayPublicKey} prints out the public key associated with a given
     90        private key
     91        """
     92        # Unencrypted key - no passphrase
     93        filename = self.mktemp()
     94        FilePath(filename).setContent(privateRSA_openssh)
     95        displayPublicKey({'filename':filename})
     96        self.assertEqual(
     97            self.stdout.getvalue().strip('\n'),
     98            publicRSA_openssh.strip(' comment')
     99        )
     100
     101        # Encrypted key should require a passphrase
     102        self.stdout.seek(0)
     103        filename = self.mktemp()
     104        FilePath(filename).setContent(privateRSA_openssh_encrypted)
     105        displayPublicKey({'filename':filename, 'pass':'encrypted'})
     106        self.assertEqual(
     107            self.stdout.getvalue().strip('\n'),
     108            publicRSA_openssh.strip(' comment')
     109        )
     110        # Encrypted key with a bad passphrase is an error
     111        self.patch(getpass, 'getpass', lambda x: 'badpassphrase')
     112        self.assertRaises(BadKeyError,
     113            displayPublicKey, {'filename':filename})
     114
     115           
     116    def test_changePassPhrase(self):
     117        """
     118        L{changePassPhrase} allows a user to change the passphrase of a
     119        private key
     120        """
     121        class _cycledGetPass(object):
     122            """
     123            Patch C{getpass.getpass} to provide old/new passphrases
     124            """
     125            def __init__(self, oldpass, newpass):
     126                self.oldpass = oldpass
     127                self.newpass = newpass
     128                self.called = 0
     129
     130            def getPass(self, *args, **kwargs):
     131                """
     132                The first time called, return oldpass
     133                Subsequent calls return newpass
     134                """
     135                if self.called == 0:
     136                    self.called += 1
     137                    return self.oldpass
     138                else:
     139                    return self.newpass
     140        altGetPass = _cycledGetPass('encrypted', 'newpass')
     141        self.patch(getpass, 'getpass', altGetPass.getPass)
     142
     143        filename = self.mktemp()
     144        FilePath(filename).setContent(privateRSA_openssh_encrypted)
     145
     146        changePassPhrase({'filename':filename})
     147        self.assertEqual(
     148            self.stdout.getvalue().strip('\n'),
     149            'Your identification has been saved with the new passphrase.'
     150        )
     151
     152        # Provide old passphrase
     153        self.stdout.seek(0)
     154        changePassPhrase({'filename':filename, 'pass':'newpass'})
     155        self.assertEqual(
     156            self.stdout.getvalue().strip('\n'),
     157            'Your identification has been saved with the new passphrase.'
     158        )
     159
     160        # Provide both old and new passphrase
     161        self.stdout.seek(0)
     162        changePassPhrase({'filename':filename, 'pass':'newpass', 'newpass':'newencrypt'})
     163        self.assertEqual(
     164            self.stdout.getvalue().strip('\n'),
     165            'Your identification has been saved with the new passphrase.'
     166        )
     167
     168        # Provide invalid old passphrase
     169        self.stdout.seek(0)
     170        self.assertRaises(SystemExit,
     171                          changePassPhrase,
     172                          {'filename':filename, 'pass':'wrongpassphrase'})
  • twisted/conch/scripts/ckeygen.py

     
    1919from twisted.python import filepath, log, usage, randbytes
    2020
    2121
     22
    2223class GeneralOptions(usage.Options):
    2324    synopsis = """Usage:    ckeygen [options]
    2425 """
     
    4041    compData = usage.Completions(
    4142        optActions={"type": usage.CompleteList(["rsa", "dsa"])})
    4243
     44
     45
    4346def run():
    4447    options = GeneralOptions()
    4548    try:
     
    6770        options.opt_help()
    6871        sys.exit(1)
    6972
     73
     74
    7075def handleError():
    7176    from twisted.python import failure
    7277    global exitStatus
     
    7580    reactor.stop()
    7681    raise
    7782
     83
     84
    7885def generateRSAkey(options):
    7986    from Crypto.PublicKey import RSA
    8087    print 'Generating public/private rsa key pair.'
    8188    key = RSA.generate(int(options['bits']), randbytes.secureRandom)
    8289    _saveKey(key, options)
    8390
     91
     92
    8493def generateDSAkey(options):
    8594    from Crypto.PublicKey import DSA
    8695    print 'Generating public/private dsa key pair.'
     
    8897    _saveKey(key, options)
    8998
    9099
     100
    91101def printFingerprint(options):
    92102    if not options['filename']:
    93103        filename = os.path.expanduser('~/.ssh/id_rsa')
     
    106116        sys.exit('bad key')
    107117
    108118
     119
    109120def changePassPhrase(options):
    110     if not options['filename']:
     121    if not options.get('filename'):
    111122        filename = os.path.expanduser('~/.ssh/id_rsa')
    112123        options['filename'] = raw_input('Enter file in which the key is (%s): ' % filename)
    113124    try:
    114125        key = keys.Key.fromFile(options['filename']).keyObject
     126    except keys.EncryptedKeyError, e:
     127        # Raised if password not supplied for an encrypted key
     128        if not options.get('pass'):
     129            options['pass'] = getpass.getpass('Enter old passphrase: ')
     130        try:
     131            key = keys.Key.fromFile(
     132                options['filename'], passphrase=options['pass']).keyObject
     133        except keys.BadKeyError, e:
     134            sys.exit('Could not change passphrase: Old passphrase error')
    115135    except keys.BadKeyError, e:
    116         if e.args[0] != 'encrypted key with no passphrase':
    117             raise
    118         else:
    119             if not options['pass']:
    120                 options['pass'] = getpass.getpass('Enter old passphrase: ')
    121             key = keys.Key.fromFile(
    122                 options['filename'], passphrase = options['pass']).keyObject
    123     if not options['newpass']:
     136        sys.exit('Could not change passphrase: %s' % (e,))
     137
     138    if not options.get('newpass'):
    124139        while 1:
    125140            p1 = getpass.getpass('Enter new passphrase (empty for no passphrase): ')
    126141            p2 = getpass.getpass('Enter same passphrase again: ')
     
    128143                break
    129144            print 'Passphrases do not match.  Try again.'
    130145        options['newpass'] = p1
    131     open(options['filename'], 'w').write(
    132         keys.Key(key).toString(passphrase=options['newpass']))
    133     print 'Your identification has been saved with the new passphrase.'
    134146
     147    try:
     148        newkeydata = keys.Key(key).toString('openssh', extra=options['newpass'])
     149    except (keys.BadKeyError, Exception), e:
     150        sys.exit('Could not change passphrase: %s' % (e,))
     151    else:
     152        open(options['filename'], 'w').write(newkeydata)
    135153
     154    try:
     155        newkey = keys.Key.fromFile(
     156            options['filename'], passphrase=options['newpass']).keyObject
     157    except (keys.EncryptedKeyError, keys.BadKeyError), e:
     158        sys.exit('Could not change passphrase: %s' % (e,))
     159    else:
     160        print 'Your identification has been saved with the new passphrase.'
     161
     162
     163
    136164def displayPublicKey(options):
    137165    if not options['filename']:
    138166        filename = os.path.expanduser('~/.ssh/id_rsa')
    139167        options['filename'] = raw_input('Enter file in which the key is (%s): ' % filename)
    140168    try:
    141169        key = keys.Key.fromFile(options['filename']).keyObject
     170    except keys.EncryptedKeyError, e:
     171        if not options.get('pass'):
     172            options['pass'] = getpass.getpass('Enter passphrase: ')
     173        key = keys.Key.fromFile(
     174            options['filename'], passphrase = options['pass']).keyObject
    142175    except keys.BadKeyError, e:
    143         if e.args[0] != 'encrypted key with no passphrase':
    144             raise
    145         else:
    146             if not options['pass']:
    147                 options['pass'] = getpass.getpass('Enter passphrase: ')
    148             key = keys.Key.fromFile(
    149                 options['filename'], passphrase = options['pass']).keyObject
    150     print keys.Key(key).public().toString()
     176        raise
     177    print keys.Key(key).public().toString('openssh')
    151178
    152179
     180
    153181def _saveKey(key, options):
    154182    if not options['filename']:
    155183        kind = keys.objectType(key)
     
    185213    print 'The key fingerprint is:'
    186214    print keyObj.fingerprint()
    187215
     216
     217
    188218if __name__ == '__main__':
    189219    run()
    190 
  • twisted/conch/ssh/keys.py

     
    1616from Crypto.Cipher import DES3, AES
    1717from Crypto.PublicKey import RSA, DSA
    1818from Crypto import Util
     19from pyasn1.error import PyAsn1Error
    1920from pyasn1.type import univ
    2021from pyasn1.codec.ber import decoder as berDecoder
    2122from pyasn1.codec.ber import encoder as berEncoder
     
    2829from twisted.conch.ssh import common, sexpy
    2930
    3031
     32
    3133class BadKeyError(Exception):
    3234    """
    3335    Raised when a key isn't what we expected from it.
     
    3638    """
    3739
    3840
     41
    3942class EncryptedKeyError(Exception):
    4043    """
    4144    Raised when an encrypted key is presented to fromString/fromFile without
     
    4346    """
    4447
    4548
     49
    4650class Key(object):
    4751    """
    4852    An object representing a key.  A key can be either a public or
     
    6367        return Class.fromString(file(filename, 'rb').read(), type, passphrase)
    6468    fromFile = classmethod(fromFile)
    6569
     70
    6671    def fromString(Class, data, type=None, passphrase=None):
    6772        """
    6873        Return a Key object corresponding to the string data.
     
    9196            return method(data, passphrase)
    9297    fromString = classmethod(fromString)
    9398
     99
    94100    def _fromString_BLOB(Class, blob):
    95101        """
    96102        Return a public key object corresponding to this public key blob.
     
    121127            raise BadKeyError('unknown blob type: %s' % keyType)
    122128    _fromString_BLOB = classmethod(_fromString_BLOB)
    123129
     130
    124131    def _fromString_PRIVATE_BLOB(Class, blob):
    125132        """
    126133        Return a private key object corresponding to this private key blob.
     
    161168            raise BadKeyError('unknown blob type: %s' % keyType)
    162169    _fromString_PRIVATE_BLOB = classmethod(_fromString_PRIVATE_BLOB)
    163170
     171
    164172    def _fromString_PUBLIC_OPENSSH(Class, data):
    165173        """
    166174        Return a public key object corresponding to this OpenSSH public key
     
    175183        return Class._fromString_BLOB(blob)
    176184    _fromString_PUBLIC_OPENSSH = classmethod(_fromString_PUBLIC_OPENSSH)
    177185
     186
    178187    def _fromString_PRIVATE_OPENSSH(Class, data, passphrase):
    179188        """
    180189        Return a private key object corresponding to this OpenSSH private key
     
    199208        @return: a C{Crypto.PublicKey.pubkey.pubkey} object
    200209        @raises BadKeyError: if
    201210            * a passphrase is provided for an unencrypted key
    202             * a passphrase is not provided for an encrypted key
    203211            * the ASN.1 encoding is incorrect
     212        @raises EncryptedKeyError: if
     213            * a passphrase is not provided for an encrypted key       
    204214        """
    205215        lines = data.strip().split('\n')
    206216        kind = lines[0][11:14]
    207217        if lines[1].startswith('Proc-Type: 4,ENCRYPTED'):  # encrypted key
     218            if not passphrase:
     219                raise EncryptedKeyError('Passphrase must be provided '
     220                                        'for an encrypted key')
     221
     222            # Determine cipher and initialization vector
    208223            try:
    209224                _, cipher_iv_info = lines[2].split(' ', 1)
    210225                cipher, ivdata = cipher_iv_info.rstrip().split(',', 1)
    211226            except ValueError:
    212227                raise BadKeyError('invalid DEK-info %r' % lines[2])
     228
    213229            if cipher == 'AES-128-CBC':
    214230                CipherClass = AES
    215231                keySize = 16
     
    222238                    raise BadKeyError('DES encrypted key with a bad IV')
    223239            else:
    224240                raise BadKeyError('unknown encryption type %r' % cipher)
     241
     242            # extract keyData for decoding
    225243            iv = ''.join([chr(int(ivdata[i:i + 2], 16))
    226244                          for i in range(0, len(ivdata), 2)])
    227             if not passphrase:
    228                 raise EncryptedKeyError('encrypted key with no passphrase')
    229245            ba = md5(passphrase + iv[:8]).digest()
    230246            bb = md5(ba + passphrase + iv[:8]).digest()
    231247            decKey = (ba + bb)[:keySize]
     
    238254        else:
    239255            b64Data = ''.join(lines[1:-1])
    240256            keyData = base64.decodestring(b64Data)
     257
    241258        try:
    242259            decodedKey = berDecoder.decode(keyData)[0]
    243         except Exception:
    244             raise BadKeyError('something wrong with decode')
     260        except PyAsn1Error, e:
     261            raise BadKeyError('Failed to decode key (Bad Passphrase?): %s' % e)
     262
    245263        if kind == 'RSA':
    246264            if len(decodedKey) == 2:  # alternate RSA key
    247265                decodedKey = decodedKey[0]
    248266            if len(decodedKey) < 6:
    249267                raise BadKeyError('RSA key failed to decode properly')
     268
    250269            n, e, d, p, q = [long(value) for value in decodedKey[1:6]]
    251270            if p > q:  # make p smaller than q
    252271                p, q = q, p
     
    258277            return Class(DSA.construct((y, g, p, q, x)))
    259278    _fromString_PRIVATE_OPENSSH = classmethod(_fromString_PRIVATE_OPENSSH)
    260279
     280
    261281    def _fromString_PUBLIC_LSH(Class, data):
    262282        """
    263283        Return a public key corresponding to this LSH public key string.
     
    284304            raise BadKeyError('unknown lsh key type %s' % sexp[1][0])
    285305    _fromString_PUBLIC_LSH = classmethod(_fromString_PUBLIC_LSH)
    286306
     307
    287308    def _fromString_PRIVATE_LSH(Class, data):
    288309        """
    289310        Return a private key corresponding to this LSH private key string.
     
    316337            raise BadKeyError('unknown lsh key type %s' % sexp[1][0])
    317338    _fromString_PRIVATE_LSH = classmethod(_fromString_PRIVATE_LSH)
    318339
     340
    319341    def _fromString_AGENTV3(Class, data):
    320342        """
    321343        Return a private key object corresponsing to the Secure Shell Key
     
    362384            raise BadKeyError("unknown key type %s" % keyType)
    363385    _fromString_AGENTV3 = classmethod(_fromString_AGENTV3)
    364386
     387
    365388    def _guessStringType(Class, data):
    366389        """
    367390        Guess the type of key in data.  The types map to _fromString_*
     
    387410                return 'blob'
    388411    _guessStringType = classmethod(_guessStringType)
    389412
     413
    390414    def __init__(self, keyObject):
    391415        """
    392416        Initialize a PublicKey with a C{Crypto.PublicKey.pubkey.pubkey}
     
    396420        """
    397421        self.keyObject = keyObject
    398422
     423
    399424    def __eq__(self, other):
    400425        """
    401426        Return True if other represents an object with the same key.
     
    405430        else:
    406431            return NotImplemented
    407432
     433
    408434    def __ne__(self, other):
    409435        """
    410436        Return True if other represents anything other than this key.
     
    414440            return result
    415441        return not result
    416442
     443
    417444    def __repr__(self):
    418445        """
    419446        Return a pretty representation of this object.
     
    438465        lines[-1] = lines[-1] + '>'
    439466        return '\n'.join(lines)
    440467
     468
    441469    def isPublic(self):
    442470        """
    443471        Returns True if this Key is a public key.
    444472        """
    445473        return not self.keyObject.has_private()
    446474
     475
    447476    def public(self):
    448477        """
    449478        Returns a version of this key containing only the public key data.
     
    452481        """
    453482        return Key(self.keyObject.publickey())
    454483
     484
    455485    def fingerprint(self):
    456486        """
    457487        Get the user presentation of the fingerprint of this L{Key}.  As
     
    474504        """
    475505        return ':'.join([x.encode('hex') for x in md5(self.blob()).digest()])
    476506
     507
    477508    def type(self):
    478509        """
    479510        Return the type of the object we wrap.  Currently this can only be
     
    490521        else:
    491522            raise RuntimeError('unknown type of key: %s' % type)
    492523
     524
    493525    def sshType(self):
    494526        """
    495527        Return the type of the object we wrap as defined in the ssh protocol.
     
    497529        """
    498530        return {'RSA': 'ssh-rsa', 'DSA': 'ssh-dss'}[self.type()]
    499531
     532
    500533    def data(self):
    501534        """
    502535        Return the values of the public key as a dictionary.
     
    510543                keyData[name] = value
    511544        return keyData
    512545
     546
    513547    def blob(self):
    514548        """
    515549        Return the public key blob for this key.  The blob is the
     
    539573                    common.MP(data['q']) + common.MP(data['g']) +
    540574                    common.MP(data['y']))
    541575
     576
    542577    def privateBlob(self):
    543578        """
    544579        Return the private key blob for this key.  The blob is the
     
    573608                    common.MP(data['q']) + common.MP(data['g']) +
    574609                    common.MP(data['y']) + common.MP(data['x']))
    575610
     611
    576612    def toString(self, type, extra=None):
    577613        """
    578614        Create a string representation of this key.  If the key is a private
     
    599635        else:
    600636            return method()
    601637
     638
    602639    def _toString_OPENSSH(self, extra):
    603640        """
    604641        Return a public or private OpenSSH string.  See
     
    606643        string formats.  If extra is present, it represents a comment for a
    607644        public key, or a passphrase for a private key.
    608645
    609         @type extra: C{str}
     646        @type extra: C{str} Comment for a public key or
     647                            Passphrase for a private key
    610648        @rtype: C{str}
    611649        """
    612650        data = self.data()
     
    646684            lines.append('-----END %s PRIVATE KEY-----' % self.type())
    647685            return '\n'.join(lines)
    648686
     687
    649688    def _toString_LSH(self):
    650689        """
    651690        Return a public or private LSH key.  See _fromString_PUBLIC_LSH and
     
    690729                                     ['y', common.MP(data['y'])[4:]],
    691730                                     ['x', common.MP(data['x'])[4:]]]]])
    692731
     732
    693733    def _toString_AGENTV3(self):
    694734        """
    695735        Return a private Secure Shell Agent v3 key.  See
     
    707747                          data['x'])
    708748            return common.NS(self.sshType()) + ''.join(map(common.MP, values))
    709749
     750
    710751    def sign(self, data):
    711752        """
    712753        Returns a signature with this Key.
     
    730771                            Util.number.long_to_bytes(sig[1], 20))
    731772        return common.NS(self.sshType()) + ret
    732773
     774
    733775    def verify(self, signature, data):
    734776        """
    735777        Returns true if the signature for data is valid for this Key.
     
    756798        return self.keyObject.verify(digest, numbers)
    757799
    758800
     801
    759802def objectType(obj):
    760803    """
    761804    Return the SSH key type corresponding to a
     
    775818        raise BadKeyError("invalid key object", obj)
    776819
    777820
     821
    778822def pkcs1Pad(data, messageLength):
    779823    """
    780824    Pad out data to messageLength according to the PKCS#1 standard.
     
    785829    return '\x01' + ('\xff' * lenPad) + '\x00' + data
    786830
    787831
     832
    788833def pkcs1Digest(data, messageLength):
    789834    """
    790835    Create a message digest using the SHA1 hash algorithm according to the
     
    796841    return pkcs1Pad(ID_SHA1 + digest, messageLength)
    797842
    798843
     844
    799845def lenSig(obj):
    800846    """
    801847    Return the length of the signature in bytes for a key object.