From 12287b3e0e95664a383902b69a0881775b550feb Mon Sep 17 00:00:00 2001 From: Robey Pointer Date: Sat, 25 Sep 2004 21:28:23 +0000 Subject: [PATCH] [project @ Arch-1:robey@lag.net--2003-public%secsh--dev--1.0--patch-87] clean up pkey interface change the pkey interface so that it's no longer possible to have a pkey that doesn't represent a valid key. (ie: no more "blank" key objects.) also add "get_bits" and "can_sign" methods to determine the key bit length and whether it can sign things (contains the "private parts") respectively. --- paramiko/dsskey.py | 84 +++++++++++++++++++-------------------- paramiko/pkey.py | 97 ++++++++++++++++++++++++---------------------- paramiko/rsakey.py | 97 +++++++++++++++++++++++++--------------------- 3 files changed, 145 insertions(+), 133 deletions(-) diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index eaefba3..09952b9 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -38,22 +38,24 @@ class DSSKey (PKey): data. """ - def __init__(self, msg=None, data=None): - self.valid = False + def __init__(self, msg=None, data=None, filename=None, password=None, vals=None): + if filename is not None: + self._from_private_key_file(filename, password) + return if (msg is None) and (data is not None): msg = Message(data) - if (msg is None) or (msg.get_string() != 'ssh-dss'): - return - self.p = msg.get_mpint() - self.q = msg.get_mpint() - self.g = msg.get_mpint() - self.y = msg.get_mpint() - self.size = len(util.deflate_long(self.p, 0)) - self.valid = True + if vals is not None: + self.p, self.q, self.g, self.y = vals + else: + if (msg is None) or (msg.get_string() != 'ssh-dss'): + raise SSHException('Invalid key') + self.p = msg.get_mpint() + self.q = msg.get_mpint() + self.g = msg.get_mpint() + self.y = msg.get_mpint() + self.size = util.bit_length(self.p) def __str__(self): - if not self.valid: - return '' m = Message() m.add_string('ssh-dss') m.add_mpint(self.p) @@ -74,6 +76,12 @@ class DSSKey (PKey): def get_name(self): return 'ssh-dss' + def get_bits(self): + return self.size + + def can_sign(self): + return hasattr(self, 'x') + def sign_ssh_data(self, randpool, data): hash = SHA.new(data).digest() dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x))) @@ -90,8 +98,6 @@ class DSSKey (PKey): return m def verify_ssh_sig(self, data, msg): - if not self.valid: - return 0 if len(str(msg)) == 40: # spies.com bug: signature has no header sig = str(msg) @@ -109,28 +115,7 @@ class DSSKey (PKey): dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q))) return dss.verify(sigM, (sigR, sigS)) - def read_private_key_file(self, filename, password=None): - # private key file contains: - # DSAPrivateKey = { version = 0, p, q, g, y, x } - self.valid = False - data = self._read_private_key_file('DSA', filename, password) - try: - keylist = BER(data).decode() - except BERException, x: - raise SSHException('Unable to parse key file: ' + str(x)) - if (type(keylist) is not list) or (len(keylist) < 6) or (keylist[0] != 0): - raise SSHException('not a valid DSA private key file (bad ber encoding)') - self.p = keylist[1] - self.q = keylist[2] - self.g = keylist[3] - self.y = keylist[4] - self.x = keylist[5] - self.size = len(util.deflate_long(self.p, 0)) - self.valid = True - def write_private_key_file(self, filename, password=None): - if not self.valid: - raise SSHException('Invalid key') keylist = [ 0, self.p, self.q, self.g, self.y, self.x ] try: b = BER() @@ -155,12 +140,29 @@ class DSSKey (PKey): @since: fearow """ dsa = DSA.generate(bits, randpool.get_bytes, progress_func) - key = DSSKey() - key.p = dsa.p - key.q = dsa.q - key.g = dsa.g - key.y = dsa.y + key = DSSKey(vals=(dsa.p, dsa.q, dsa.g, dsa.y)) key.x = dsa.x - key.valid = True return key generate = staticmethod(generate) + + + ### internals... + + + def _from_private_key_file(self, filename, password): + # private key file contains: + # DSAPrivateKey = { version = 0, p, q, g, y, x } + data = self._read_private_key_file('DSA', filename, password) + try: + keylist = BER(data).decode() + except BERException, x: + raise SSHException('Unable to parse key file: ' + str(x)) + if (type(keylist) is not list) or (len(keylist) < 6) or (keylist[0] != 0): + raise SSHException('not a valid DSA private key file (bad ber encoding)') + self.p = keylist[1] + self.q = keylist[2] + self.g = keylist[3] + self.y = keylist[4] + self.x = keylist[5] + self.size = util.bit_length(self.p) + diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 8faacc5..a7dc8a8 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -55,7 +55,10 @@ class PKey (object): type. @type msg: L{Message} @param data: an optional string containing a public key of this type - @type data: string + @type data: str + + @raise SSHException: if a key cannot be created from the C{data} or + C{msg} given, or no key was passed in. """ pass @@ -66,7 +69,7 @@ class PKey (object): re-create the key object later. @return: string representation of an SSH key message. - @rtype: string + @rtype: str """ return '' @@ -94,10 +97,30 @@ class PKey (object): @return: name of this private key type, in SSH terminology (for example, C{"ssh-rsa"}). - @rtype: string + @rtype: str """ return '' + def get_bits(self): + """ + Return the number of significant bits in this key. This is useful + for judging the relative security of a key. + + @return: bits in the key. + @rtype: int + """ + return 0 + + def can_sign(self): + """ + Return C{True} if this key has the private part necessary for signing + data. + + @return: C{True} if this is a private key. + @rtype: bool + """ + return False + def get_fingerprint(self): """ Return an MD5 fingerprint of the public part of this key. Nothing @@ -105,7 +128,7 @@ class PKey (object): @return: a 16-byte string (binary) of the MD5 fingerprint, in SSH format. - @rtype: string + @rtype: str """ return MD5.new(str(self)).digest() @@ -116,7 +139,7 @@ class PKey (object): public key files or recognized host keys. @return: a base64 string containing the public part of the key. - @rtype: string + @rtype: str @since: fearow """ @@ -130,7 +153,7 @@ class PKey (object): @param randpool: a secure random number generator. @type randpool: L{Crypto.Util.randpool.RandomPool} @param data: the data to sign. - @type data: string + @type data: str @return: an SSH signature message. @rtype: L{Message} """ @@ -142,7 +165,7 @@ class PKey (object): that data, verify that it was signed with this key. @param data: the data that was signed. - @type data: string + @type data: str @param msg: an SSH signature message @type msg: L{Message} @return: C{True} if the signature verifies correctly; C{False} @@ -150,40 +173,21 @@ class PKey (object): @rtype: boolean """ return False - - def read_private_key_file(self, filename, password=None): - """ - Read private key contents from a file into this object. If the private - key is encrypted and C{password} is not C{None}, the given password - will be used to decrypt the key (otherwise L{PasswordRequiredException} - is thrown). - - @param filename: name of the file to read. - @type filename: string - @param password: an optional password to use to decrypt the key file, - if it's encrypted. - @type password: string - - @raise IOError: if there was an error reading the file. - @raise PasswordRequiredException: if the private key file is - encrypted, and C{password} is C{None}. - @raise SSHException: if the key file is invalid. - """ - raise exception('Not implemented in PKey') - + def from_private_key_file(cl, filename, password=None): """ - Create a key object by reading a private key file. This is roughly - equivalent to creating a new key object and then calling - L{read_private_key_file} on it. Through the magic of python, this - factory method will exist in all subclasses of PKey (such as L{RSAKey} - or L{DSSKey}), but is useless on the abstract PKey class. + Create a key object by reading a private key file. If the private + key is encrypted and C{password} is not C{None}, the given password + will be used to decrypt the key (otherwise L{PasswordRequiredException} + is thrown). Through the magic of python, this factory method will + exist in all subclasses of PKey (such as L{RSAKey} or L{DSSKey}), but + is useless on the abstract PKey class. @param filename: name of the file to read. - @type filename: string + @type filename: str @param password: an optional password to use to decrypt the key file, if it's encrypted - @type password: string + @type password: str @return: a new key object based on the given private key. @rtype: L{PKey} @@ -194,8 +198,7 @@ class PKey (object): @since: fearow """ - key = cl() - key.read_private_key_file(filename, password) + key = cl(filename=filename, password=password) return key from_private_key_file = classmethod(from_private_key_file) @@ -205,9 +208,9 @@ class PKey (object): C{None}, the key is encrypted before writing. @param filename: name of the file to write. - @type filename: string + @type filename: str @param password: an optional password to use to encrypt the key file. - @type password: string + @type password: str @raise IOError: if there was an error writing the file. @raise SSHException: if the key is invalid. @@ -225,14 +228,14 @@ class PKey (object): the key (otherwise L{PasswordRequiredException} is thrown). @param tag: C{"RSA"} or C{"DSA"}, the tag used to mark the data block. - @type tag: string + @type tag: str @param filename: name of the file to read. - @type filename: string + @type filename: str @param password: an optional password to use to decrypt the key file, if it's encrypted. - @type password: string + @type password: str @return: data blob that makes up the private key. - @rtype: string + @rtype: str @raise IOError: if there was an error reading the file. @raise PasswordRequiredException: if the private key file is @@ -296,13 +299,13 @@ class PKey (object): a password is given, DES-EDE3-CBC is used. @param tag: C{"RSA"} or C{"DSA"}, the tag used to mark the data block. - @type tag: string + @type tag: str @param filename: name of the file to write. - @type filename: string + @type filename: str @param data: data blob that makes up the private key. - @type data: string + @type data: str @param password: an optional password to use to encrypt the file. - @type password: string + @type password: str @raise IOError: if there was an error writing the file. """ diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index aa85724..e9e7d01 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -39,20 +39,22 @@ class RSAKey (PKey): data. """ - def __init__(self, msg=None, data=''): - self.valid = False + def __init__(self, msg=None, data='', filename=None, password=None, vals=None): + if filename is not None: + self._from_private_key_file(filename, password) + return if (msg is None) and (data is not None): msg = Message(data) - if (msg is None) or (msg.get_string() != 'ssh-rsa'): - return - self.e = msg.get_mpint() - self.n = msg.get_mpint() - self.size = len(util.deflate_long(self.n, 0)) - self.valid = True + if vals is not None: + self.e, self.n = vals + else: + if (msg is None) or (msg.get_string() != 'ssh-rsa'): + raise SSHException('Invalid key') + self.e = msg.get_mpint() + self.n = msg.get_mpint() + self.size = util.bit_length(self.n) def __str__(self): - if not self.valid: - return '' m = Message() m.add_string('ssh-rsa') m.add_mpint(self.e) @@ -68,14 +70,11 @@ class RSAKey (PKey): def get_name(self): return 'ssh-rsa' - def _pkcs1imify(self, data): - """ - turn a 20-byte SHA1 hash into a blob of data as large as the key's N, - using PKCS1's \"emsa-pkcs1-v1_5\" encoding. totally bizarre. - """ - SHA1_DIGESTINFO = '\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14' - filler = '\xff' * (self.size - len(SHA1_DIGESTINFO) - len(data) - 3) - return '\x00\x01' + filler + '\x00' + SHA1_DIGESTINFO + data + def get_bits(self): + return self.size + + def can_sign(self): + return hasattr(self, 'd') def sign_ssh_data(self, randpool, data): hash = SHA.new(data).digest() @@ -87,7 +86,7 @@ class RSAKey (PKey): return m def verify_ssh_sig(self, data, msg): - if (not self.valid) or (msg.get_string() != 'ssh-rsa'): + if msg.get_string() != 'ssh-rsa': return False sig = util.inflate_long(msg.get_string(), 1) # verify the signature by SHA'ing the data and encrypting it using the @@ -97,29 +96,7 @@ class RSAKey (PKey): rsa = RSA.construct((long(self.n), long(self.e))) return rsa.verify(hash, (sig,)) - def read_private_key_file(self, filename, password=None): - # private key file contains: - # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } - self.valid = False - data = self._read_private_key_file('RSA', filename, password) - try: - keylist = BER(data).decode() - except BERException: - raise SSHException('Unable to parse key file') - if (type(keylist) is not list) or (len(keylist) < 4) or (keylist[0] != 0): - raise SSHException('Not a valid RSA private key file (bad ber encoding)') - self.n = keylist[1] - self.e = keylist[2] - self.d = keylist[3] - # not really needed - self.p = keylist[4] - self.q = keylist[5] - self.size = len(util.deflate_long(self.n, 0)) - self.valid = True - def write_private_key_file(self, filename, password=None): - if not self.valid: - raise SSHException('Invalid key') keylist = [ 0, self.n, self.e, self.d, self.p, self.q, self.d % (self.p - 1), self.d % (self.q - 1), util.mod_inverse(self.q, self.p) ] @@ -146,12 +123,42 @@ class RSAKey (PKey): @since: fearow """ rsa = RSA.generate(bits, randpool.get_bytes, progress_func) - key = RSAKey() - key.n = rsa.n - key.e = rsa.e + key = RSAKey(vals=(rsa.e, rsa.n)) key.d = rsa.d key.p = rsa.p key.q = rsa.q - key.valid = True return key generate = staticmethod(generate) + + + ### internals... + + + def _pkcs1imify(self, data): + """ + turn a 20-byte SHA1 hash into a blob of data as large as the key's N, + using PKCS1's \"emsa-pkcs1-v1_5\" encoding. totally bizarre. + """ + SHA1_DIGESTINFO = '\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14' + size = len(util.deflate_long(self.n, 0)) + filler = '\xff' * (size - len(SHA1_DIGESTINFO) - len(data) - 3) + return '\x00\x01' + filler + '\x00' + SHA1_DIGESTINFO + data + + def _from_private_key_file(self, filename, password): + # private key file contains: + # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } + data = self._read_private_key_file('RSA', filename, password) + try: + keylist = BER(data).decode() + except BERException: + raise SSHException('Unable to parse key file') + if (type(keylist) is not list) or (len(keylist) < 4) or (keylist[0] != 0): + raise SSHException('Not a valid RSA private key file (bad ber encoding)') + self.n = keylist[1] + self.e = keylist[2] + self.d = keylist[3] + # not really needed + self.p = keylist[4] + self.q = keylist[5] + self.size = util.bit_length(self.n) +