[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.
This commit is contained in:
Robey Pointer 2004-09-25 21:28:23 +00:00
parent 0737ea2ca4
commit 12287b3e0e
3 changed files with 145 additions and 133 deletions

View File

@ -38,22 +38,24 @@ class DSSKey (PKey):
data. data.
""" """
def __init__(self, msg=None, data=None): def __init__(self, msg=None, data=None, filename=None, password=None, vals=None):
self.valid = False if filename is not None:
self._from_private_key_file(filename, password)
return
if (msg is None) and (data is not None): if (msg is None) and (data is not None):
msg = Message(data) msg = Message(data)
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'): if (msg is None) or (msg.get_string() != 'ssh-dss'):
return raise SSHException('Invalid key')
self.p = msg.get_mpint() self.p = msg.get_mpint()
self.q = msg.get_mpint() self.q = msg.get_mpint()
self.g = msg.get_mpint() self.g = msg.get_mpint()
self.y = msg.get_mpint() self.y = msg.get_mpint()
self.size = len(util.deflate_long(self.p, 0)) self.size = util.bit_length(self.p)
self.valid = True
def __str__(self): def __str__(self):
if not self.valid:
return ''
m = Message() m = Message()
m.add_string('ssh-dss') m.add_string('ssh-dss')
m.add_mpint(self.p) m.add_mpint(self.p)
@ -74,6 +76,12 @@ class DSSKey (PKey):
def get_name(self): def get_name(self):
return 'ssh-dss' 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): def sign_ssh_data(self, randpool, data):
hash = SHA.new(data).digest() hash = SHA.new(data).digest()
dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x))) 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 return m
def verify_ssh_sig(self, data, msg): def verify_ssh_sig(self, data, msg):
if not self.valid:
return 0
if len(str(msg)) == 40: if len(str(msg)) == 40:
# spies.com bug: signature has no header # spies.com bug: signature has no header
sig = str(msg) 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))) dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q)))
return dss.verify(sigM, (sigR, sigS)) 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): 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 ] keylist = [ 0, self.p, self.q, self.g, self.y, self.x ]
try: try:
b = BER() b = BER()
@ -155,12 +140,29 @@ class DSSKey (PKey):
@since: fearow @since: fearow
""" """
dsa = DSA.generate(bits, randpool.get_bytes, progress_func) dsa = DSA.generate(bits, randpool.get_bytes, progress_func)
key = DSSKey() key = DSSKey(vals=(dsa.p, dsa.q, dsa.g, dsa.y))
key.p = dsa.p
key.q = dsa.q
key.g = dsa.g
key.y = dsa.y
key.x = dsa.x key.x = dsa.x
key.valid = True
return key return key
generate = staticmethod(generate) 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)

View File

@ -55,7 +55,10 @@ class PKey (object):
type. type.
@type msg: L{Message} @type msg: L{Message}
@param data: an optional string containing a public key of this type @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 pass
@ -66,7 +69,7 @@ class PKey (object):
re-create the key object later. re-create the key object later.
@return: string representation of an SSH key message. @return: string representation of an SSH key message.
@rtype: string @rtype: str
""" """
return '' return ''
@ -94,10 +97,30 @@ class PKey (object):
@return: name of this private key type, in SSH terminology (for @return: name of this private key type, in SSH terminology (for
example, C{"ssh-rsa"}). example, C{"ssh-rsa"}).
@rtype: string @rtype: str
""" """
return '' 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): def get_fingerprint(self):
""" """
Return an MD5 fingerprint of the public part of this key. Nothing 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 @return: a 16-byte string (binary) of the MD5 fingerprint, in SSH
format. format.
@rtype: string @rtype: str
""" """
return MD5.new(str(self)).digest() return MD5.new(str(self)).digest()
@ -116,7 +139,7 @@ class PKey (object):
public key files or recognized host keys. public key files or recognized host keys.
@return: a base64 string containing the public part of the key. @return: a base64 string containing the public part of the key.
@rtype: string @rtype: str
@since: fearow @since: fearow
""" """
@ -130,7 +153,7 @@ class PKey (object):
@param randpool: a secure random number generator. @param randpool: a secure random number generator.
@type randpool: L{Crypto.Util.randpool.RandomPool} @type randpool: L{Crypto.Util.randpool.RandomPool}
@param data: the data to sign. @param data: the data to sign.
@type data: string @type data: str
@return: an SSH signature message. @return: an SSH signature message.
@rtype: L{Message} @rtype: L{Message}
""" """
@ -142,7 +165,7 @@ class PKey (object):
that data, verify that it was signed with this key. that data, verify that it was signed with this key.
@param data: the data that was signed. @param data: the data that was signed.
@type data: string @type data: str
@param msg: an SSH signature message @param msg: an SSH signature message
@type msg: L{Message} @type msg: L{Message}
@return: C{True} if the signature verifies correctly; C{False} @return: C{True} if the signature verifies correctly; C{False}
@ -151,39 +174,20 @@ class PKey (object):
""" """
return False 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): def from_private_key_file(cl, filename, password=None):
""" """
Create a key object by reading a private key file. This is roughly Create a key object by reading a private key file. If the private
equivalent to creating a new key object and then calling key is encrypted and C{password} is not C{None}, the given password
L{read_private_key_file} on it. Through the magic of python, this will be used to decrypt the key (otherwise L{PasswordRequiredException}
factory method will exist in all subclasses of PKey (such as L{RSAKey} is thrown). Through the magic of python, this factory method will
or L{DSSKey}), but is useless on the abstract PKey class. 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. @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, @param password: an optional password to use to decrypt the key file,
if it's encrypted if it's encrypted
@type password: string @type password: str
@return: a new key object based on the given private key. @return: a new key object based on the given private key.
@rtype: L{PKey} @rtype: L{PKey}
@ -194,8 +198,7 @@ class PKey (object):
@since: fearow @since: fearow
""" """
key = cl() key = cl(filename=filename, password=password)
key.read_private_key_file(filename, password)
return key return key
from_private_key_file = classmethod(from_private_key_file) from_private_key_file = classmethod(from_private_key_file)
@ -205,9 +208,9 @@ class PKey (object):
C{None}, the key is encrypted before writing. C{None}, the key is encrypted before writing.
@param filename: name of the file to write. @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. @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 IOError: if there was an error writing the file.
@raise SSHException: if the key is invalid. @raise SSHException: if the key is invalid.
@ -225,14 +228,14 @@ class PKey (object):
the key (otherwise L{PasswordRequiredException} is thrown). the key (otherwise L{PasswordRequiredException} is thrown).
@param tag: C{"RSA"} or C{"DSA"}, the tag used to mark the data block. @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. @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, @param password: an optional password to use to decrypt the key file,
if it's encrypted. if it's encrypted.
@type password: string @type password: str
@return: data blob that makes up the private key. @return: data blob that makes up the private key.
@rtype: string @rtype: str
@raise IOError: if there was an error reading the file. @raise IOError: if there was an error reading the file.
@raise PasswordRequiredException: if the private key file is @raise PasswordRequiredException: if the private key file is
@ -296,13 +299,13 @@ class PKey (object):
a password is given, DES-EDE3-CBC is used. a password is given, DES-EDE3-CBC is used.
@param tag: C{"RSA"} or C{"DSA"}, the tag used to mark the data block. @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. @param filename: name of the file to write.
@type filename: string @type filename: str
@param data: data blob that makes up the private key. @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. @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. @raise IOError: if there was an error writing the file.
""" """

View File

@ -39,20 +39,22 @@ class RSAKey (PKey):
data. data.
""" """
def __init__(self, msg=None, data=''): def __init__(self, msg=None, data='', filename=None, password=None, vals=None):
self.valid = False if filename is not None:
self._from_private_key_file(filename, password)
return
if (msg is None) and (data is not None): if (msg is None) and (data is not None):
msg = Message(data) msg = Message(data)
if vals is not None:
self.e, self.n = vals
else:
if (msg is None) or (msg.get_string() != 'ssh-rsa'): if (msg is None) or (msg.get_string() != 'ssh-rsa'):
return raise SSHException('Invalid key')
self.e = msg.get_mpint() self.e = msg.get_mpint()
self.n = msg.get_mpint() self.n = msg.get_mpint()
self.size = len(util.deflate_long(self.n, 0)) self.size = util.bit_length(self.n)
self.valid = True
def __str__(self): def __str__(self):
if not self.valid:
return ''
m = Message() m = Message()
m.add_string('ssh-rsa') m.add_string('ssh-rsa')
m.add_mpint(self.e) m.add_mpint(self.e)
@ -68,14 +70,11 @@ class RSAKey (PKey):
def get_name(self): def get_name(self):
return 'ssh-rsa' return 'ssh-rsa'
def _pkcs1imify(self, data): def get_bits(self):
""" return self.size
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. def can_sign(self):
""" return hasattr(self, 'd')
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 sign_ssh_data(self, randpool, data): def sign_ssh_data(self, randpool, data):
hash = SHA.new(data).digest() hash = SHA.new(data).digest()
@ -87,7 +86,7 @@ class RSAKey (PKey):
return m return m
def verify_ssh_sig(self, data, msg): 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 return False
sig = util.inflate_long(msg.get_string(), 1) sig = util.inflate_long(msg.get_string(), 1)
# verify the signature by SHA'ing the data and encrypting it using the # 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))) rsa = RSA.construct((long(self.n), long(self.e)))
return rsa.verify(hash, (sig,)) 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): 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, keylist = [ 0, self.n, self.e, self.d, self.p, self.q,
self.d % (self.p - 1), self.d % (self.q - 1), self.d % (self.p - 1), self.d % (self.q - 1),
util.mod_inverse(self.q, self.p) ] util.mod_inverse(self.q, self.p) ]
@ -146,12 +123,42 @@ class RSAKey (PKey):
@since: fearow @since: fearow
""" """
rsa = RSA.generate(bits, randpool.get_bytes, progress_func) rsa = RSA.generate(bits, randpool.get_bytes, progress_func)
key = RSAKey() key = RSAKey(vals=(rsa.e, rsa.n))
key.n = rsa.n
key.e = rsa.e
key.d = rsa.d key.d = rsa.d
key.p = rsa.p key.p = rsa.p
key.q = rsa.q key.q = rsa.q
key.valid = True
return key return key
generate = staticmethod(generate) 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)