paramiko / paramiko

The leading native Python SSHv2 protocol library.
http://paramiko.org
GNU Lesser General Public License v2.1
9.15k stars 2.01k forks source link

[BUG] - Sometimes loading an ECDSAKey or DSSKey with a comment of certain lengths results in "Invalid key". #2445

Open Styca96 opened 2 months ago

Styca96 commented 2 months ago

Are you using paramiko as a client or server?

Client

What feature(s) aren't working right?

Keys/auth

What version(s) of paramiko are you using?

3.4.1

What version(s) of Python are you using?

3.10.12

What operating system and version are you using?

WSL on Windows 11

If you're connecting as a client, which SSH server are you connecting to?

No response

If you're using paramiko as part of another tool, which tool/version?

No response

Expected/desired behavior

Load any private file with ECDSAKey without getting an error

Actual behavior

Sometimes when I try lo load a private ECDSAKey from a file I get an error:

paramiko.ssh_exception.SSHException: Invalid key curve identifier

The problemis is in the function _unpad_openssh in pkey.py module.

def _unpad_openssh(data):
    # At the moment, this is only used for unpadding private keys on disk. This
    # really ought to be made constant time (possibly by upstreaming this logic
    # into pyca/cryptography).

    padding_length = data[-1]
    if 0x20 <= padding_length < 0x7:
        return data  # no padding, last byte part comment (printable ascii)
    if padding_length > 15:
        raise SSHException("Invalid key")
    for i in range(padding_length):
        if data[i - padding_length] != i + 1:
            raise SSHException("Invalid key")
    return data[:-padding_length]

When the padding_length is equal to 0, this function return an empty list that cause an Exception. I don't kwon about padding and comment so I don't kwon if it is the correct behaviour.

The key is valid if we change this line:

if 0x20 <= padding_length < 0x7F:

in this one:

if 0x20 <= padding_length < 0x7F or padding_lenght == 0x00:

How to reproduce

By using this snippet of code you can see that sometimes the load of the key fail

import os
import paramiko

def path(_type, n):
    return f'/tmp/key_{_type}_{n}'

passphrase = ''  #  an empty passphrase is much faster
comment = ""
CYCLE = 10

for _ in range(CYCLE):
    for _type in ['rsa','ecdsa','ed25519','dsa']:
        try:
            os.system('rm -f %s' % path(_type, 0))
        except: 
            pass
        os.system('ssh-keygen -t %s -N "%s" -C "%s" -f %s > /dev/null' % (_type, passphrase, comment, path(_type, 0)))

        #  see which keys can be loaded

        s = comment
        try:
            match _type:
                case 'rsa':
                    pkey = paramiko.RSAKey.from_private_key_file(filename=path(_type,0), password=passphrase)
                case 'ecdsa':
                    pkey = paramiko.ECDSAKey.from_private_key_file(filename=path(_type,0), password=passphrase)
                case 'ed25519':
                    pkey = paramiko.Ed25519Key.from_private_key_file(filename=path(_type, 0), password=passphrase)
                case 'dsa':
                    pkey = paramiko.DSSKey.from_private_key_file(filename=path(_type, 0), password=passphrase)
            print('%8s  %11s  %2d  %s' % (_type, '+', len(s), s))
        except paramiko.SSHException as se:
            print('%8s  %11s  %2d  %s' % (_type, str(se), len(s), s))

Output:

     rsa            +   0  
   ecdsa            +   0  
 ed25519            +   0  
     dsa  unpack requires a buffer of 4 bytes   0  
     rsa            +   0  
   ecdsa            +   0  
 ed25519            +   0  
     dsa            +   0  
     rsa            +   0  
   ecdsa  Invalid key curve identifier   0  
 ed25519            +   0  
     dsa            +   0  
     rsa            +   0  
   ecdsa  Invalid key curve identifier   0  
 ed25519            +   0  
     dsa            +   0  
     rsa            +   0  
   ecdsa            +   0  
 ed25519            +   0  
     dsa            +   0  
     rsa            +   0  
   ecdsa  Invalid key curve identifier   0  
 ed25519            +   0  
     dsa            +   0  
     rsa            +   0  
   ecdsa            +   0  
 ed25519            +   0  
     dsa            +   0  
     rsa            +   0  
   ecdsa  Invalid key curve identifier   0  
 ed25519            +   0  
     dsa            +   0  
     rsa            +   0  
   ecdsa            +   0  
 ed25519            +   0  
     dsa            +   0  
     rsa            +   0  
   ecdsa  Invalid key curve identifier   0  
 ed25519            +   0  
     dsa            +   0  

Anything else?

System Info: $ ssh -V OpenSSH_8.9p1 Ubuntu-3ubuntu0.10, OpenSSL 3.0.2 15 Mar 2022

jun66j5 commented 2 months ago

The same issue is able to reproduced by the comment which is passed to ssh-keygen. Also, the pullreq cannot fix it.

$ ssh-keygen -N '' -C "$(python -c 'import os; os.write(1, b"\x07" * 2)')" -t ecdsa -b 521 -f /dev/shm/id_ecdsa
Generating public/private ecdsa key pair.
Your identification has been saved in /dev/shm/id_ecdsa
Your public key has been saved in /dev/shm/id_ecdsa.pub
The key fingerprint is:
SHA256:7t0wCAeyd4jDr/OLmLkQaAjYt7TMEB08eLMqNdo+4X4
The key's randomart image is:
+---[ECDSA 521]---+
|  .+..           |
|....*            |
|o o.++.          |
|+ oB.* o         |
|+= oX + S        |
|+.+  + = .       |
|.+ .  . o o      |
| .++Eo . . +     |
| .*+ooo.. . .    |
+----[SHA256]-----+

$ ~/venv/paramiko-dev/bin/python -c 'from paramiko import ECDSAKey as K; K.from_private_key_file("/dev/shm/id_ecdsa")'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/jun66j5/src/paramiko/paramiko/pkey.py", line 435, in from_private_key_file
    key = cls(filename=filename, password=password)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jun66j5/src/paramiko/paramiko/ecdsakey.py", line 127, in __init__
    self._from_private_key_file(filename, password)
  File "/home/jun66j5/src/paramiko/paramiko/ecdsakey.py", line 283, in _from_private_key_file
    data = self._read_private_key_file("EC", filename, password)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jun66j5/src/paramiko/paramiko/pkey.py", line 509, in _read_private_key_file
    data = self._read_private_key(tag, f, password)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jun66j5/src/paramiko/paramiko/pkey.py", line 540, in _read_private_key
    data = self._read_private_key_openssh(lines[start:end], password)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jun66j5/src/paramiko/paramiko/pkey.py", line 684, in _read_private_key_openssh
    return _unpad_openssh(keydata)
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jun66j5/src/paramiko/paramiko/pkey.py", line 74, in _unpad_openssh
    raise SSHException("Invalid key")
paramiko.ssh_exception.SSHException: Invalid key
$ ssh-keygen -N '' -C "$(python -c 'import os; os.write(1, b"\x07" * 5)')" -t ed25519 -f /dev/shm/id_ed25519  Generating public/private ed25519 key pair.
Your identification has been saved in /dev/shm/id_ed25519
Your public key has been saved in /dev/shm/id_ed25519.pub
The key fingerprint is:
SHA256:XQUGIEo0c65ug+Wn35GPQnrW1e8nr/6xWBLryT5Gjz0
The key's randomart image is:
+--[ED25519 256]--+
|   .= o ....o..  |
|   . * .   . .   |
|    . .     .    |
|     .   . .     |
|    o   S .. .   |
|   =  .  .. ..o  |
|  . =o..o.  .++o |
|   ..++..+  o+BE+|
|    .+..o . oO=B+|
+----[SHA256]-----+

$ ~/venv/paramiko-dev/bin/python -c 'from paramiko import Ed25519Key as K; K.from_private_key_file("/dev/shm/id_ed25519")'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/jun66j5/src/paramiko/paramiko/pkey.py", line 435, in from_private_key_file
    key = cls(filename=filename, password=password)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jun66j5/src/paramiko/paramiko/ed25519key.py", line 65, in __init__
    signing_key = self._parse_signing_key_data(data, password)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jun66j5/src/paramiko/paramiko/ed25519key.py", line 140, in _parse_signing_key_data
    message = Message(_unpad_openssh(private_data))
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jun66j5/src/paramiko/paramiko/pkey.py", line 74, in _unpad_openssh
    raise SSHException("Invalid key")
paramiko.ssh_exception.SSHException: Invalid key