# Copyright (C) 2003-2007 Robey Pointer # # This file is part of paramiko. # # Paramiko is free software; you can redistribute it and/or modify it under the # terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ RSA keys. """ import os from hashlib import sha1 from Crypto.PublicKey import RSA from paramiko import util from paramiko.common import max_byte, zero_byte, one_byte from paramiko.message import Message from paramiko.ber import BER, BERException from paramiko.pkey import PKey from paramiko.py3compat import long from paramiko.ssh_exception import SSHException SHA1_DIGESTINFO = b'\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14' class RSAKey (PKey): """ Representation of an RSA key which can be used to sign and verify SSH2 data. """ def __init__(self, msg=None, data=None, filename=None, password=None, vals=None, file_obj=None): self.n = None self.e = None self.d = None self.p = None self.q = None if file_obj is not None: self._from_private_key(file_obj, password) return 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 vals is not None: self.e, self.n = vals else: if msg is None: raise SSHException('Key object may not be empty') if msg.get_text() != 'ssh-rsa': raise SSHException('Invalid key') self.e = msg.get_mpint() self.n = msg.get_mpint() self.size = util.bit_length(self.n) def asbytes(self): m = Message() m.add_string('ssh-rsa') m.add_mpint(self.e) m.add_mpint(self.n) return m.asbytes() def __str__(self): return self.asbytes() def __hash__(self): h = hash(self.get_name()) h = h * 37 + hash(self.e) h = h * 37 + hash(self.n) return hash(h) def get_name(self): return 'ssh-rsa' def get_bits(self): return self.size def can_sign(self): return self.d is not None def sign_ssh_data(self, data): digest = sha1(data).digest() rsa = RSA.construct((long(self.n), long(self.e), long(self.d))) sig = util.deflate_long(rsa.sign(self._pkcs1imify(digest), bytes())[0], 0) m = Message() m.add_string('ssh-rsa') m.add_string(sig) return m def verify_ssh_sig(self, data, msg): if msg.get_text() != 'ssh-rsa': return False sig = util.inflate_long(msg.get_binary(), True) # verify the signature by SHA'ing the data and encrypting it using the # public key. some wackiness ensues where we "pkcs1imify" the 20-byte # hash into a string as long as the RSA key. hash_obj = util.inflate_long(self._pkcs1imify(sha1(data).digest()), True) rsa = RSA.construct((long(self.n), long(self.e))) return rsa.verify(hash_obj, (sig,)) def _encode_key(self): if (self.p is None) or (self.q is None): raise SSHException('Not enough key info to write private key file') 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)] try: b = BER() b.encode(keylist) except BERException: raise SSHException('Unable to create ber encoding of key') return b.asbytes() def write_private_key_file(self, filename, password=None): self._write_private_key_file('RSA', filename, self._encode_key(), password) def write_private_key(self, file_obj, password=None): self._write_private_key('RSA', file_obj, self._encode_key(), password) @staticmethod def generate(bits, progress_func=None): """ Generate a new private RSA key. This factory function can be used to generate a new host key or authentication key. :param int bits: number of bits the generated key should be. :param function progress_func: an optional function to call at key points in key generation (used by ``pyCrypto.PublicKey``). :return: new `.RSAKey` private key """ rsa = RSA.generate(bits, os.urandom, progress_func) key = RSAKey(vals=(rsa.e, rsa.n)) key.d = rsa.d key.p = rsa.p key.q = rsa.q return key ### 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. """ size = len(util.deflate_long(self.n, 0)) filler = max_byte * (size - len(SHA1_DIGESTINFO) - len(data) - 3) return zero_byte + one_byte + filler + zero_byte + SHA1_DIGESTINFO + data def _from_private_key_file(self, filename, password): data = self._read_private_key_file('RSA', filename, password) self._decode_key(data) def _from_private_key(self, file_obj, password): data = self._read_private_key('RSA', file_obj, password) self._decode_key(data) def _decode_key(self, data): # private key file contains: # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } 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)