Skip to content

Instantly share code, notes, and snippets.

@adiroiban
Created December 16, 2020 16:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adiroiban/1af3df6061a42b102a4faa94a808b056 to your computer and use it in GitHub Desktop.
Save adiroiban/1af3df6061a42b102a4faa94a808b056 to your computer and use it in GitHub Desktop.
Read putty private key in Python
# Code extracted from my private project, but released here as public code.
@classmethod
def _fromString_PRIVATE_PUTTY(cls, data, passphrase):
"""
Read a private Putty key.
Format is:
PuTTY-User-Key-File-2: ssh-rsa
Encryption: aes256-cbc | none
Comment: SINGLE_LINE_COMMENT
Public-Lines: PUBLIC_LINES
< base64 public part always in plain >
Private-Lines: 8
< base64 private part >
Private-MAC: 1398fbfc7ce307d9ee0e42851f183f88c728398f
Pulic part RSA:
* string type (ssh-rsa)
* mpint e
* mpint n
Private part RSA:
* mpint d
* mpint q
* mpint p
* mpint u
Pulic part DSA:
* string type (ssh-dss)
* mpint p
* mpint q
* mpint g
* mpint v`
Private part DSA:
* mpint x
Private part is padded for encryption.
Encryption key is composed of concatenating, up to block size:
* uint32 sequence, starting from 0
* passphrase
Lines are terminated by CRLF, although CR-only and LF-only are
tolerated on input.
Only version 2 is supported.
Version 2 was introduced in PuTTY 0.52.
Version 1 was an in-development format used in 0.52 snapshot
"""
lines = data.strip().splitlines()
key_type = lines[0][22:].strip().lower()
if key_type not in [b'ssh-rsa', b'ssh-dss']:
raise BadKeyError('Unsupported key type: %r' % key_type[:30])
encryption_type = lines[1][11:].strip().lower()
if encryption_type == b'none':
if passphrase:
raise BadKeyError('PuTTY key not encrypted')
elif encryption_type != b'aes256-cbc':
raise BadKeyError(
'Unsupported encryption type: %r' % encryption_type[:30])
comment = lines[2][9:].strip()
public_count = int(lines[3][14:].strip())
base64_content = ''.join(lines[
4:
4 + public_count
])
public_blob = base64.decodestring(base64_content)
public_type, public_payload = common.getNS(public_blob)
if public_type.lower() != key_type:
raise BadKeyError(
'Mismatch key type. Header has %r, public has %r' % (
key_type[:30], public_type[:30]))
# We skip 4 lines so far and the total public lines.
private_start_line = 4 + public_count
private_count = int(lines[private_start_line][15:].strip())
base64_content = ''.join(lines[
private_start_line + 1:
private_start_line + 1 + private_count
])
private_blob = base64.decodestring(base64_content)
private_mac = lines[-1][12:].strip()
hmac_key = PUTTY_HMAC_KEY
encryption_key = None
if encryption_type == b'aes256-cbc':
if not passphrase:
raise EncryptedKeyError(
'Passphrase must be provided for an encrypted key.')
hmac_key += passphrase
encryption_key = cls._getPuttyAES256EncryptionKey(passphrase)
private_blob = AES.new(
encryption_key, mode=AES.MODE_CBC, IV=b'\x00' * 16).decrypt(
private_blob)
# I have no idea why these values are packed form HMAC as net strings.
hmac_data = (
common.NS(key_type) +
common.NS(encryption_type) +
common.NS(comment) +
common.NS(public_blob) +
common.NS(private_blob)
)
hmac_key = sha1(hmac_key).digest()
computed_mac = hmac.new(hmac_key, hmac_data, sha1).hexdigest()
if private_mac != computed_mac:
if encryption_key:
raise EncryptedKeyError('Bad password or HMAC mismatch.')
else:
raise BadKeyError(
'HMAC mismatch: file declare %s, actual is %s' % (
private_mac, computed_mac))
if key_type == b'ssh-rsa':
e, n, _ = common.getMP(public_payload, count=2)
d, q, p, u, _ = common.getMP(private_blob, count=4)
return cls(RSA.construct((n, e, d, p, q, u)))
if key_type == b'ssh-dss':
p, q, g, y, _ = common.getMP(public_payload, count=4)
x, _ = common.getMP(private_blob)
return cls(DSA.construct((y, g, p, q, x)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment