CVE-2022-31124: Generation of Error Message Containing Sensitive Information (CWE-209)
value: openssh_key_parser is an open source Python package providing utilities to parse and pack OpenSSH private and public key files. In versions prior to 0.0.6 if a field of a key is shorter than it is declared to be, the parser raises an error with a message containing the raw field value. An attacker able to modify the declared length of a key's sensitive field can thus expose the raw value of that field. Users are advised to upgrade to version 0.0.6, which no longer includes the raw field value in the error message. There are no known workarounds for this issue.
CWE description: The product generates an error message that includes sensitive information about its environment, users, or associated data.
CVSS v3 base score: 6.5
Published: 2022-07-06T18:15Z
Repository: https://github.com/scottcwang/openssh_key_parser
Language: Python
Files changed: openssh_key/private_key_list.py
Commit date: 2022-06-22 10:59:49 -0400
Commit message: Improved error handling to prevent unhandled exceptions in calling code.

Diff: diff --git a/openssh_key/private_key_list.py b/openssh_key/private_key_list.py
index 72613ae..0899378 100644
--- a/openssh_key/private_key_list.py
+++ b/openssh_key/private_key_list.py
@@ -219,122 +219,131 @@ class PrivateKeyList(BaseList):
         Returns:
             A :any:`PrivateKeyList` object containing the private key list.
 
         Raises:
             ValueError: The provided byte string is not an ``openssh-key-v1``
-                key list or the declared key count is negative.
+                key list, when the declared key count is negative, or when an
+                EOF is found while parsing the key.
+
             UserWarning: The check numbers in the decrypted private byte string
                 do not match (likely due to an incorrect passphrase), the key
                 type or parameter values of a private key do not match that of
                 the corresponding public key in the list, or the padding bytes
                 at the end of the decrypted private byte string are not as
                 expected.
         """
-        byte_stream = PascalStyleByteStream(byte_string)
+        try:
+            byte_stream = PascalStyleByteStream(byte_string)
 
-        header = byte_stream.read_from_format_instructions_dict(
-            cls.HEADER_FORMAT_INSTRUCTIONS_DICT
-        )
+            header = byte_stream.read_from_format_instructions_dict(
+                cls.HEADER_FORMAT_INSTRUCTIONS_DICT
+            )
+
+            if header['auth_magic'] != b'openssh-key-v1\x00':
+                raise ValueError('Not an openssh-key-v1 key')
 
-        if header['auth_magic'] != b'openssh-key-v1\x00':
-            raise ValueError('Not an openssh-key-v1 key')
+            num_keys = header['num_keys']
 
-        num_keys = header['num_keys']
+            if num_keys < 0:
+                raise ValueError('Cannot parse negative number of keys')
 
-        if num_keys < 0:
-            raise ValueError('Cannot parse negative number of keys')
+            public_key_list = []
+            for i in range(num_keys):
+                public_key_bytes = byte_stream.read_from_format_instruction(
+                    PascalStyleFormatInstruction.BYTES
+                )
+                public_key_list.append(
+                    PublicKey.from_bytes(public_key_bytes)
+                )
 
-        public_key_list = []
-        for i in range(num_keys):
-            public_key_bytes = byte_stream.read_from_format_instruction(
+            cipher_bytes = byte_stream.read_from_format_instruction(
                 PascalStyleFormatInstruction.BYTES
             )
-            public_key_list.append(
-                PublicKey.from_bytes(public_key_bytes)
-            )
 
-        cipher_bytes = byte_stream.read_from_format_instruction(
-            PascalStyleFormatInstruction.BYTES
-        )
-
-        kdf_class = get_kdf_options_class(header['kdf'])
-        kdf_options = kdf_class(
-            PascalStyleByteStream(
-                header['kdf_options']
-            ).read_from_format_instructions_dict(
-                kdf_class.FORMAT_INSTRUCTIONS_DICT
+            kdf_class = get_kdf_options_class(header['kdf'])
+            kdf_options = kdf_class(
+                PascalStyleByteStream(
+                    header['kdf_options']
+                ).read_from_format_instructions_dict(
+                    kdf_class.FORMAT_INSTRUCTIONS_DICT
+                )
             )
-        )
 
-        cipher_class = get_cipher_class(header['cipher'])
+            cipher_class = get_cipher_class(header['cipher'])
 
-        if kdf_class == NoneKDFOptions:
-            passphrase = ''
-        elif passphrase is None:
-            passphrase = getpass.getpass('Key passphrase: ')
+            if kdf_class == NoneKDFOptions:
+                passphrase = ''
+            elif passphrase is None:
+                passphrase = getpass.getpass('Key passphrase: ')
 
-        if issubclass(cipher_class, ConfidentialityIntegrityCipher):
-            cipher_bytes += byte_stream.read_fixed_bytes(
-                cipher_class.TAG_LENGTH
-            )
-
-        decipher_bytes = cipher_class.decrypt(
-            kdf_class(kdf_options),
-            passphrase,
-            cipher_bytes
-        )
-
-        decipher_byte_stream = PascalStyleByteStream(decipher_bytes)
+            if issubclass(cipher_class, ConfidentialityIntegrityCipher):
+                cipher_bytes += byte_stream.read_fixed_bytes(
+                    cipher_class.TAG_LENGTH
+                )
 
-        decipher_bytes_header = \
-            decipher_byte_stream.read_from_format_instructions_dict(
-                cls.DECIPHER_BYTES_HEADER_FORMAT_INSTRUCTIONS_DICT
+            decipher_bytes = cipher_class.decrypt(
+                kdf_class(kdf_options),
+                passphrase,
+                cipher_bytes
             )
 
-        if decipher_bytes_header['check_int_1'] \
-                != decipher_bytes_header['check_int_2']:
-            warnings.warn('Cipher header check numbers do not match')
+            decipher_byte_stream = PascalStyleByteStream(decipher_bytes)
 
-        initlist = []
-        for i in range(num_keys):
-            initlist.append(
-                PublicPrivateKeyPair(
-                    public_key_list[i],
-                    PrivateKey.from_byte_stream(decipher_byte_stream)
-                )
-            )
-            if initlist[i].public.header['key_type'] \
-                    != initlist[i].private.header['key_type']:
-                warnings.warn(
-                    f'Inconsistency between private and public '
-                    f'key types for key {i}'
+            decipher_bytes_header = \
+                decipher_byte_stream.read_from_format_instructions_dict(
+                    cls.DECIPHER_BYTES_HEADER_FORMAT_INSTRUCTIONS_DICT
                 )
-            if not all(
-                (
-                    initlist[i].public.params[k] ==
-                    initlist[i].private.params[k]
-                ) for k in (
-                    initlist[i].public.params.keys() &
-                    initlist[i].private.params.keys()
+
+            if decipher_bytes_header['check_int_1'] \
+                    != decipher_bytes_header['check_int_2']:
+                warnings.warn('Cipher header check numbers do not match')
+
+            initlist = []
+            for i in range(num_keys):
+                initlist.append(
+                    PublicPrivateKeyPair(
+                        public_key_list[i],
+                        PrivateKey.from_byte_stream(decipher_byte_stream)
+                    )
                 )
+                if initlist[i].public.header['key_type'] \
+                        != initlist[i].private.header['key_type']:
+                    warnings.warn(
+                        f'Inconsistency between private and public '
+                        f'key types for key {i}'
+                    )
+                if not all(
+                    (
+                        initlist[i].public.params[k] ==
+                        initlist[i].private.params[k]
+                    ) for k in (
+                        initlist[i].public.params.keys() &
+                        initlist[i].private.params.keys()
+                    )
+                ):
+                    warnings.warn(
+                        f'Inconsistency between private and public '
+                        f'values for key {i}'
+                    )
+
+            decipher_padding = decipher_byte_stream.read()
+
+            if (
+                len(decipher_byte_stream.getvalue()) %
+                    cipher_class.BLOCK_SIZE != 0
+            ) or not (
+                bytes(
+                    range(1, 1 + cipher_class.BLOCK_SIZE)
+                ).startswith(decipher_padding)
             ):
-                warnings.warn(
-                    f'Inconsistency between private and public '
-                    f'values for key {i}'
-                )
-
-        decipher_padding = decipher_byte_stream.read()
-
-        if (
-            len(decipher_byte_stream.getvalue()) %
-                cipher_class.BLOCK_SIZE != 0
-        ) or not (
-            bytes(
-                range(1, 1 + cipher_class.BLOCK_SIZE)
-            ).startswith(decipher_padding)
-        ):
-            warnings.warn('Incorrect padding at end of ciphertext')
+                warnings.warn('Incorrect padding at end of ciphertext')
+        except ValueError as e:
+            raise e
+        except EOFError as e:
+            raise ValueError('Premature EOF detected while parsing key.')
+        except e:
+            raise ValueError('Unexpected error condition reached.')
 
         return cls(
             initlist,
             byte_string,
             header,

Vulnerable code:                 key list or the declared key count is negative.
        byte_stream = PascalStyleByteStream(byte_string)
        header = byte_stream.read_from_format_instructions_dict(
            cls.HEADER_FORMAT_INSTRUCTIONS_DICT
        )
        if header['auth_magic'] != b'openssh-key-v1\x00':
            raise ValueError('Not an openssh-key-v1 key')
        num_keys = header['num_keys']
        if num_keys < 0:
            raise ValueError('Cannot parse negative number of keys')
        public_key_list = []
        for i in range(num_keys):
            public_key_bytes = byte_stream.read_from_format_instruction(
            public_key_list.append(
                PublicKey.from_bytes(public_key_bytes)
            )
        cipher_bytes = byte_stream.read_from_format_instruction(
            PascalStyleFormatInstruction.BYTES
        )

        kdf_class = get_kdf_options_class(header['kdf'])
        kdf_options = kdf_class(
            PascalStyleByteStream(
                header['kdf_options']
            ).read_from_format_instructions_dict(
                kdf_class.FORMAT_INSTRUCTIONS_DICT
        )
        cipher_class = get_cipher_class(header['cipher'])
        if kdf_class == NoneKDFOptions:
            passphrase = ''
        elif passphrase is None:
            passphrase = getpass.getpass('Key passphrase: ')
        if issubclass(cipher_class, ConfidentialityIntegrityCipher):
            cipher_bytes += byte_stream.read_fixed_bytes(
                cipher_class.TAG_LENGTH
            )

        decipher_bytes = cipher_class.decrypt(
            kdf_class(kdf_options),
            passphrase,
            cipher_bytes
        )

        decipher_byte_stream = PascalStyleByteStream(decipher_bytes)
        decipher_bytes_header = \
            decipher_byte_stream.read_from_format_instructions_dict(
                cls.DECIPHER_BYTES_HEADER_FORMAT_INSTRUCTIONS_DICT
        if decipher_bytes_header['check_int_1'] \
                != decipher_bytes_header['check_int_2']:
            warnings.warn('Cipher header check numbers do not match')
        initlist = []
        for i in range(num_keys):
            initlist.append(
                PublicPrivateKeyPair(
                    public_key_list[i],
                    PrivateKey.from_byte_stream(decipher_byte_stream)
                )
            )
            if initlist[i].public.header['key_type'] \
                    != initlist[i].private.header['key_type']:
                warnings.warn(
                    f'Inconsistency between private and public '
                    f'key types for key {i}'
            if not all(
                (
                    initlist[i].public.params[k] ==
                    initlist[i].private.params[k]
                ) for k in (
                    initlist[i].public.params.keys() &
                    initlist[i].private.params.keys()
                warnings.warn(
                    f'Inconsistency between private and public '
                    f'values for key {i}'
                )

        decipher_padding = decipher_byte_stream.read()

        if (
            len(decipher_byte_stream.getvalue()) %
                cipher_class.BLOCK_SIZE != 0
        ) or not (
            bytes(
                range(1, 1 + cipher_class.BLOCK_SIZE)
            ).startswith(decipher_padding)
        ):
            warnings.warn('Incorrect padding at end of ciphertext')
Fixed code:                 key list, when the declared key count is negative, or when an
                EOF is found while parsing the key.

        try:
            byte_stream = PascalStyleByteStream(byte_string)
            header = byte_stream.read_from_format_instructions_dict(
                cls.HEADER_FORMAT_INSTRUCTIONS_DICT
            )

            if header['auth_magic'] != b'openssh-key-v1\x00':
                raise ValueError('Not an openssh-key-v1 key')
            num_keys = header['num_keys']
            if num_keys < 0:
                raise ValueError('Cannot parse negative number of keys')
            public_key_list = []
            for i in range(num_keys):
                public_key_bytes = byte_stream.read_from_format_instruction(
                    PascalStyleFormatInstruction.BYTES
                )
                public_key_list.append(
                    PublicKey.from_bytes(public_key_bytes)
                )
            cipher_bytes = byte_stream.read_from_format_instruction(
            kdf_class = get_kdf_options_class(header['kdf'])
            kdf_options = kdf_class(
                PascalStyleByteStream(
                    header['kdf_options']
                ).read_from_format_instructions_dict(
                    kdf_class.FORMAT_INSTRUCTIONS_DICT
                )
            cipher_class = get_cipher_class(header['cipher'])
            if kdf_class == NoneKDFOptions:
                passphrase = ''
            elif passphrase is None:
                passphrase = getpass.getpass('Key passphrase: ')
            if issubclass(cipher_class, ConfidentialityIntegrityCipher):
                cipher_bytes += byte_stream.read_fixed_bytes(
                    cipher_class.TAG_LENGTH
                )
            decipher_bytes = cipher_class.decrypt(
                kdf_class(kdf_options),
                passphrase,
                cipher_bytes
            decipher_byte_stream = PascalStyleByteStream(decipher_bytes)
            decipher_bytes_header = \
                decipher_byte_stream.read_from_format_instructions_dict(
                    cls.DECIPHER_BYTES_HEADER_FORMAT_INSTRUCTIONS_DICT

            if decipher_bytes_header['check_int_1'] \
                    != decipher_bytes_header['check_int_2']:
                warnings.warn('Cipher header check numbers do not match')

            initlist = []
            for i in range(num_keys):
                initlist.append(
                    PublicPrivateKeyPair(
                        public_key_list[i],
                        PrivateKey.from_byte_stream(decipher_byte_stream)
                    )
                if initlist[i].public.header['key_type'] \
                        != initlist[i].private.header['key_type']:
                    warnings.warn(
                        f'Inconsistency between private and public '
                        f'key types for key {i}'
                    )
                if not all(
                    (
                        initlist[i].public.params[k] ==
                        initlist[i].private.params[k]
                    ) for k in (
                        initlist[i].public.params.keys() &
                        initlist[i].private.params.keys()
                    )
                ):
                    warnings.warn(
                        f'Inconsistency between private and public '
                        f'values for key {i}'
                    )

            decipher_padding = decipher_byte_stream.read()

            if (
                len(decipher_byte_stream.getvalue()) %
                    cipher_class.BLOCK_SIZE != 0
            ) or not (
                bytes(
                    range(1, 1 + cipher_class.BLOCK_SIZE)
                ).startswith(decipher_padding)
                warnings.warn('Incorrect padding at end of ciphertext')
        except ValueError as e:
            raise e
        except EOFError as e:
            raise ValueError('Premature EOF detected while parsing key.')
        except e:
            raise ValueError('Unexpected error condition reached.')