Parse and pack OpenSSH private and public key files
Project description
openssh_key_parser
Please don't use versions 0.0.1 - 0.0.5, which are affected by CVE-2022-31124, a High severity vulnerability. Upgrade to at least version 0.0.6.
This repository provides openssh_key, a Python package providing utilities to
parse and pack OpenSSH private and public key files.
It supports parsing and packing keys of all types currently supported by OpenSSH:
ssh-rsassh-ed25519ssh-dssecdsa-sha2-{nistp256,nistp384,nistp521}sk-{ed25519,ecdsa-sha2-nistp256}@openssh.comFIDO/U2F security keys*-cert-v01@openssh.comcertificates
It can optionally encrypt and decrypt private keys using the bcrypt key
derivation function and any cipher currently supported by OpenSSH:
aes{128,192,256}-{ctr,cbc}aes{128,256}-gcm@openssh.comchacha20-poly1305@openssh.com3des-cbc
It is compliant with the OpenSSH private key vendor extension, in particular supporting multiple keys in a private key file.
Keys can be converted to corresponding classes provided by:
Check out the documentation.
Example
See what is in an OpenSSH key file
$ ssh-keygen -t ed25519 -N secret_passphrase -C my_comment -f test_id_ed25519
Generating public/private ed25519 key pair.
Your identification has been saved in test_id_ed25519.
Your public key has been saved in test_id_ed25519.pub.
The key fingerprint is:
SHA256:NbhmjL1RfNkYWOb63Rq2ugIzsmr9zLoSsn5ZUPa3Qic my_comment
The key's randomart image is:
+--[ED25519 256]--+
| o+ |
| o o.o = |
| o .. = = . |
| . +E+o+ |
| ...S+.. |
| . . oo=o.. . . |
| o = o.= . + .|
| . = oo . . + |
| ..o.oo++ .ooo |
+----[SHA256]-----+
$ python -m openssh_key test_id_ed25519 --passphrase secret_passphrase
{
"data": [
[
{
"header": {
"key_type": "ssh-ed25519"
},
"params": {
"data": {
"public": "b'\\xd0\\x96\\x7f\\xcd\\x02K\\x8e\\xfe)\\xc1\\xd1p\\x00\\xbd\\xcf\\xe3\\xf6\\xe8\\x91\\xc9\\x84\\xf5\\x9e\\xacL\\xe0\\x9c/2i8R'"
}
},
"footer": {},
"clear": {}
},
{
"header": {
"key_type": "ssh-ed25519"
},
"params": {
"data": {
"public": "b'\\xd0\\x96\\x7f\\xcd\\x02K\\x8e\\xfe)\\xc1\\xd1p\\x00\\xbd\\xcf\\xe3\\xf6\\xe8\\x91\\xc9\\x84\\xf5\\x9e\\xacL\\xe0\\x9c/2i8R'",
"private_public": "b'\\x99\\x08;#\\x07\\xb970\\xc3\\xeb\\\\\\x0e\\xe4\\xc1\\x1a\\xd4\\x12\\xa6\\x05\\x88v\\xae\\x9e9\\xc28\\x1a\\xb8\\x92b0\\x8c\\xd0\\x96\\x7f\\xcd\\x02K\\x8e\\xfe)\\xc1\\xd1p\\x00\\xbd\\xcf\\xe3\\xf6\\xe8\\x91\\xc9\\x84\\xf5\\x9e\\xacL\\xe0\\x9c/2i8R'"
}
},
"footer": {
"comment": "my_comment"
},
"clear": {}
}
]
],
"byte_string": "b'openssh-key-v1\\x00\\x00\\x00\\x00\\naes256-ctr\\x00\\x00\\x00\\x06bcrypt\\x00\\x00\\x00\\x18\\x00\\x00\\x00\\x10\\xfa\\xca\\x90\\x04\\x96\\x83\\xbb\\xe9\\x00\\x05\\'\\x8ev\\x06,t\\x00\\x00\\x00\\x10\\x00\\x00\\x00\\x01\\x00\\x00\\x003\\x00\\x00\\x00\\x0bssh-ed25519\\x00\\x00\\x00 \\xd0\\x96\\x7f\\xcd\\x02K\\x8e\\xfe)\\xc1\\xd1p\\x00\\xbd\\xcf\\xe3\\xf6\\xe8\\x91\\xc9\\x84\\xf5\\x9e\\xacL\\xe0\\x9c/2i8R\\x00\\x00\\x00\\x90\\xf9Iu\\x91\\x7f\\x82V\\xe1E2\\x98\\x17\\x82g8jmdy\\xabZz\\x85\\xa5\\xa1\\x05%\\x9a\\xdds\\x18/\\xd2[\\xad\\xd6\\xc6\\xe3\\xb14\\x92\\xa85\\x05BI#7\\x93#\\x07\\x9cu\\xe4\\xcb\\xccJ\\xe2\\x98\\xb4\\xde\\xf8\\x96\\x8f/)2P\\xef\\x02DgO\\x1d\\xe9\\x82\\xc2\\xa0D\\xbe\\x88\\xef\\xb4\\x86\\xbb\"I\\xc0\\x10\\x91\\xebT|\\x9a:\\xaf\\r6MZq\\xba\\xa7|r\\x17=\\xe7\\xaa\\xdeq.\\xa4\\xef\\xdc!\\x12N\\xdf\\x14\\x98\\xec-,~6\\x81.\\xa0\\xec\\xfe[.\\x17\\xf3z\\xbf\\xa1Q\\xbf\\xda\\xb3\\xeeY'",
"header": {
"auth_magic": "b'openssh-key-v1\\x00'",
"cipher": "aes256-ctr",
"kdf": "bcrypt",
"kdf_options": "b\"\\x00\\x00\\x00\\x10\\xfa\\xca\\x90\\x04\\x96\\x83\\xbb\\xe9\\x00\\x05'\\x8ev\\x06,t\\x00\\x00\\x00\\x10\"",
"num_keys": 1
},
"cipher_bytes": "b'\\xf9Iu\\x91\\x7f\\x82V\\xe1E2\\x98\\x17\\x82g8jmdy\\xabZz\\x85\\xa5\\xa1\\x05%\\x9a\\xdds\\x18/\\xd2[\\xad\\xd6\\xc6\\xe3\\xb14\\x92\\xa85\\x05BI#7\\x93#\\x07\\x9cu\\xe4\\xcb\\xccJ\\xe2\\x98\\xb4\\xde\\xf8\\x96\\x8f/)2P\\xef\\x02DgO\\x1d\\xe9\\x82\\xc2\\xa0D\\xbe\\x88\\xef\\xb4\\x86\\xbb\"I\\xc0\\x10\\x91\\xebT|\\x9a:\\xaf\\r6MZq\\xba\\xa7|r\\x17=\\xe7\\xaa\\xdeq.\\xa4\\xef\\xdc!\\x12N\\xdf\\x14\\x98\\xec-,~6\\x81.\\xa0\\xec\\xfe[.\\x17\\xf3z\\xbf\\xa1Q\\xbf\\xda\\xb3\\xeeY'",
"kdf_options": {
"salt": "b\"\\xfa\\xca\\x90\\x04\\x96\\x83\\xbb\\xe9\\x00\\x05'\\x8ev\\x06,t\"",
"rounds": 16
},
"decipher_bytes": "b'\\xb1\\xe5\\x03+\\xb1\\xe5\\x03+\\x00\\x00\\x00\\x0bssh-ed25519\\x00\\x00\\x00 \\xd0\\x96\\x7f\\xcd\\x02K\\x8e\\xfe)\\xc1\\xd1p\\x00\\xbd\\xcf\\xe3\\xf6\\xe8\\x91\\xc9\\x84\\xf5\\x9e\\xacL\\xe0\\x9c/2i8R\\x00\\x00\\x00@\\x99\\x08;#\\x07\\xb970\\xc3\\xeb\\\\\\x0e\\xe4\\xc1\\x1a\\xd4\\x12\\xa6\\x05\\x88v\\xae\\x9e9\\xc28\\x1a\\xb8\\x92b0\\x8c\\xd0\\x96\\x7f\\xcd\\x02K\\x8e\\xfe)\\xc1\\xd1p\\x00\\xbd\\xcf\\xe3\\xf6\\xe8\\x91\\xc9\\x84\\xf5\\x9e\\xacL\\xe0\\x9c/2i8R\\x00\\x00\\x00\\nmy_comment\\x01\\x02\\x03'",
"decipher_bytes_header": {
"check_int_1": 2984575787,
"check_int_2": 2984575787
},
"decipher_padding": "b'\\x01\\x02\\x03'"
}
$ python -m openssh_key test_id_ed25519.pub
[
{
"header": {
"key_type": "ssh-ed25519"
},
"params": {
"data": {
"public": "b'\\xd0\\x96\\x7f\\xcd\\x02K\\x8e\\xfe)\\xc1\\xd1p\\x00\\xbd\\xcf\\xe3\\xf6\\xe8\\x91\\xc9\\x84\\xf5\\x9e\\xacL\\xe0\\x9c/2i8R'"
}
},
"footer": {},
"clear": {
"key_type": "ssh-ed25519",
"comment": "my_comment"
}
}
]
Manipulate a private key file
$ python
>>> import openssh_key.private_key_list as pkl
>>> sk_list = pkl.PrivateKeyList.from_string(open('test_id_ed25519').read(), passphrase='secret_passphrase')
>>> sk_list
[PublicPrivateKeyPair(public=<openssh_key.key.PublicKey object at 0x7fd0808f6400>, private=<openssh_key.key.PrivateKey object at 0x7fd07f781640>)]
>>> sk_list[0].private.footer
{'comment': 'my_comment'}
>>> sk_list[0].private.footer['comment'] = 'new_comment'
>>> _ = open('modified_test_id_ed25519', 'w').write(sk_list.pack_string(passphrase='new_secret_passphrase'))
>>> _ = open('modified_test_id_ed25519.pub', 'w').write(sk_list[0].public.pack_public_string())
Generate a private key
>>> pk_sk_pair = pkl.PublicPrivateKeyPair.generate('ssh-rsa', 'comment')
>>> generated_sk_list = pkl.PrivateKeyList.from_list([pk_sk_pair], cipher='aes256-ctr', kdf='bcrypt')
>>> _ = open('generated_test_id_rsa', 'w').write(generated_sk_list.pack_string(passphrase='secret_passphrase'))
>>> _ = open('generated_test_id_rsa.pub', 'w').write(generated_pk_key.pack_public_string())
Convert keys to external classes
>>> import cryptography.hazmat.primitives.asymmetric.rsa as rsa
>>> sk_params.convert_to(rsa.RSAPrivateKey)
<cryptography.hazmat.backends.openssl.rsa._RSAPrivateKey object at 0x7f74522fadc0>
>>> sk_params.convert_to(rsa.RSAPublicKey)
<cryptography.hazmat.backends.openssl.rsa._RSAPublicKey object at 0x7f74522faac0>
Tests
The package provides a full-coverage test suite and complete type annotations.
$ git clone https://github.com/scottcwang/openssh_key_parser.git
$ pip install .
$ pip install -r requirements-dev.txt
$ pytest
Changelog
0.0.7
- Fix - Support Python 3.9; migrate
setup.cfgtopyproject.toml(@KOLANICH)
0.0.6
- Fix - Don't dump raw bytes in
pascal_style_byte_streamexception message. Fixes CVE-2022-31124, a High severity vulnerability. (@mike-arnica) - Fix - Raise
ValueErrorinstead ofNotImplementedError
0.0.5
- Fix - Fix incorrect key type name for
ecdsa-sha2-* - Fix - Fix bad import of
nacl.signingined25519 - Breaking change - The
create_{cipher,kdf_options,public_key_params,private_key_params}factory methods are renamed toget_*_class - Migrate to
pyproject.toml
0.0.4
- Breaking change - The
encryptanddecryptmethods of theCipherclass now take as arguments an instance of theKDFclass and apassphrase(and no longercipher_keyandinitialization_vector) - Breaking change - The
kdfmodule has been renamed tokdf_options - Support
aes128-ctr,aes192-ctr,aes128-cbc,aes192-cbc,aes256-cbc,3des-cbc,aes128-gcm@openssh.com,aes256-gcm@openssh.com, andchacha20-poly1305@openssh.comciphers for private key encryption - Add a
generatestatic method toPublicPrivateKeyPair - Fix - Write an additional newline at the end of a private key file, which
is required by OpenSSH
ssh-keygen
0.0.3
- 0.0.2 and on requires Python >=3.10 (@FloLie)
0.0.2
- Support
ssh-dss,ssh-ecdsa-*,sk-*@openssh.com(FIDO/U2F security key) and*-cert-v01@openssh.com(certificate) key types
0.0.1
Initial release, supporting:
ssh-rsaandssh-ed25519key typesnoneandaes256-ctrciphers for private keysnoneandbcryptkey derivation functions for ciphers- OpenSSH key formats
Security
I'm grateful to Mike Doyle (@mike-arnica), Head of Security Research at
Arnica.io, for performing a security review of
version 0.0.5 of openssh_key_parser and finding
CVE-2022-31124.
Here's Mike's blog post
about the process he undertook.
To report a vulnerability, please open a new draft security advisory.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file openssh_key_parser-0.0.7-py3-none-any.whl.
File metadata
- Download URL: openssh_key_parser-0.0.7-py3-none-any.whl
- Upload date:
- Size: 49.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.9.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f4cc05b4ac0b10054254d7aafb484c755d6f4137772647c19a0a5ddc37b49b92
|
|
| MD5 |
0a156927ebc528cf401b0548d2e23b0e
|
|
| BLAKE2b-256 |
a7b74c04dd63e945bb5d69715c51c3a46360600a451e9f90f384721b6d4b3276
|