CVE-2022-31124 (Python, published 2022-07-06T18:15Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 6.5)
Weakness: CWE-209 \u2014 Generation of Error Message Containing Sensitive Information
The product generates an error message that includes sensitive information about its environment, users, or associated data.
Repository: https://github.com/scottcwang/openssh_key_parser
Files changed:
openssh_key/private_key_list.py
Commit message: Improved error handling to prevent unhandled exceptions in calling code.


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.')