Possible leak of key's raw field if declared length is incorrect in openssh_key_parser
Description
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.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openssh-key-parserPyPI | < 0.0.6 | 0.0.6 |
Affected products
1- Range: < 0.0.6
Patches
3274447f91b40Merge pull request #5 from mike-arnica/security-review
5 files changed · +151 −87
openssh_key/pascal_style_byte_stream.py+2 −1 modified@@ -236,7 +236,8 @@ def read_fixed_bytes(self, num_bytes: int) -> bytes: """ read_bytes = self.read(num_bytes) if len(read_bytes) < num_bytes: - raise EOFError(read_bytes) + raise EOFError("Fewer than 'num_bytes' bytes remaining in the " + "underlying bytestream") return read_bytes def read_pascal_bytes(self, string_length_size: int) -> bytes:
openssh_key/private_key_list.py+95 −86 modified@@ -221,118 +221,127 @@ def from_bytes( 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,
tests/fuzzer/fuzz_b64.py+23 −0 added@@ -0,0 +1,23 @@ +#!/usr/bin/env python3.10 + +import atheris,sys +import sys +with atheris.instrument_imports(): + #import openssh_key + from openssh_key.private_key_list import PrivateKeyList + +@atheris.instrument_func +def TestOneInput(data): + key = bytes("-----BEGIN OPENSSH PRIVATE KEY-----\nopenssh-key-v1", 'utf-8') \ + + data \ + + bytes("\n-----END OPENSSH PRIVATE KEY-----\n", 'utf-8') + try: + parsed = PrivateKeyList.from_string(key, None) + except ValueError as e: + if e.args[0] == "Not an openssh private key": + pass + + +if __name__ == "__main__": + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz()
tests/fuzzer/fuzz_valid_magic.py+27 −0 added@@ -0,0 +1,27 @@ +#!/usr/bin/env python3.10 + +import atheris +import sys, re +from base64 import b64encode + +with atheris.instrument_imports(): + #import openssh_key + from openssh_key.private_key_list import PrivateKeyList + +@atheris.instrument_func +def TestOneInput(data): + headered_data = bytes("openssh-key-v1", 'utf-8') + b = bytes("-----BEGIN OPENSSH PRIVATE KEY-----\n", 'utf-8') \ + + re.sub(b"(.{70})", b"\\1\n", b64encode(headered_data), 0, re.DOTALL) \ + + bytes("\n-----END OPENSSH PRIVATE KEY-----\n", 'utf-8') + key = b.decode("utf-8") + try: + parsed = PrivateKeyList.from_string(key, None) + except ValueError as e: + if not e.args[0] == "Unexpected error condition reached.": + pass + + +if __name__ == "__main__": + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz()
tests/fuzzer/requirements.txt+4 −0 added@@ -0,0 +1,4 @@ +bcrypt@3.2.2 +cffi@1.15.0 +pycparser@2.21 +atheris@2.0.11
d5b53b4b7e76Improved error handling to prevent unhandled exceptions in calling code.
1 file changed · +95 −86
openssh_key/private_key_list.py+95 −86 modified@@ -221,118 +221,127 @@ def from_bytes( 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,
26e0a471e9fdChanged an exception message to prevent possible disclosures of keying material.
1 file changed · +2 −1
openssh_key/pascal_style_byte_stream.py+2 −1 modified@@ -236,7 +236,8 @@ def read_fixed_bytes(self, num_bytes: int) -> bytes: """ read_bytes = self.read(num_bytes) if len(read_bytes) < num_bytes: - raise EOFError(read_bytes) + raise EOFError("Fewer than 'num_bytes' bytes remaining in the " + "underlying bytestream") return read_bytes def read_pascal_bytes(self, string_length_size: int) -> bytes:
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-hm37-9xh2-q499ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-31124ghsaADVISORY
- github.com/pypa/advisory-database/tree/main/vulns/openssh-key-parser/PYSEC-2022-233.yamlghsaWEB
- github.com/scottcwang/openssh_key_parser/commit/26e0a471e9fdb23e635bc3014cf4cbd2323a08d3ghsax_refsource_MISCWEB
- github.com/scottcwang/openssh_key_parser/commit/274447f91b4037b7050ae634879b657554523b39ghsax_refsource_MISCWEB
- github.com/scottcwang/openssh_key_parser/commit/d5b53b4b7e76c5b666fc657019dbf864fb04076cghsax_refsource_MISCWEB
- github.com/scottcwang/openssh_key_parser/pull/5ghsax_refsource_MISCWEB
- github.com/scottcwang/openssh_key_parser/security/advisories/GHSA-hm37-9xh2-q499ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.