summaryrefslogtreecommitdiff
path: root/paramiko/pkey.py
diff options
context:
space:
mode:
authorJared Hobbs <jared@pyhacker.com>2018-11-27 17:22:59 -0700
committerJared Hobbs <jared@pyhacker.com>2018-11-27 17:22:59 -0700
commiteff204faf5624c51b7ac96b9b93e4ce9622f853a (patch)
tree8cfd853320df944d7fd9ca7b272c22079af277e7 /paramiko/pkey.py
parent6656f5453cedf9d9e497a6f49a25f8fc683b8551 (diff)
downloadparamiko-eff204faf5624c51b7ac96b9b93e4ce9622f853a.tar.gz
add support for new OpenSSH private key format
This work is based off the work done in https://github.com/paramiko/paramiko/pull/618
Diffstat (limited to 'paramiko/pkey.py')
-rw-r--r--paramiko/pkey.py191
1 files changed, 179 insertions, 12 deletions
diff --git a/paramiko/pkey.py b/paramiko/pkey.py
index fa014800..4e56233f 100644
--- a/paramiko/pkey.py
+++ b/paramiko/pkey.py
@@ -24,6 +24,10 @@ import base64
from binascii import unhexlify
import os
from hashlib import md5
+import re
+import struct
+
+import bcrypt
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
@@ -31,7 +35,8 @@ from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher
from paramiko import util
from paramiko.common import o600
-from paramiko.py3compat import u, encodebytes, decodebytes, b, string_types
+from paramiko.py3compat import u, encodebytes, decodebytes, b, string_types,\
+ byte_ord
from paramiko.ssh_exception import SSHException, PasswordRequiredException
from paramiko.message import Message
@@ -62,6 +67,12 @@ class PKey(object):
"mode": modes.CBC,
},
}
+ PRIVATE_KEY_FORMAT_ORIGINAL = 1
+ PRIVATE_KEY_FORMAT_OPENSSH = 2
+ BEGIN_TAG = re.compile(
+ '^-{5}BEGIN (RSA|DSA|EC|OPENSSH) PRIVATE KEY-{5}\s*$'
+ )
+ END_TAG = re.compile('^-{5}END (RSA|DSA|EC|OPENSSH) PRIVATE KEY-{5}\s*$')
def __init__(self, msg=None, data=None):
"""
@@ -281,29 +292,59 @@ class PKey(object):
def _read_private_key(self, tag, f, password=None):
lines = f.readlines()
+
+ # find the BEGIN tag
start = 0
- beginning_of_key = "-----BEGIN " + tag + " PRIVATE KEY-----"
- while start < len(lines) and lines[start].strip() != beginning_of_key:
+ m = self.BEGIN_TAG.match(lines[start])
+ line_range = len(lines) - 1
+ while start < line_range and not m:
start += 1
+ m = self.BEGIN_TAG.match(lines[start])
+ start += 1
+ keytype = m.group(1)
if start >= len(lines):
raise SSHException("not a valid " + tag + " private key file")
+
+ # find the END tag
+ end = start
+ m = self.END_TAG.match(lines[end])
+ while end < line_range and not m:
+ end += 1
+ m = self.END_TAG.match(lines[end])
+
+ if keytype == tag:
+ data = self._read_private_key_old_format(
+ lines,
+ password,
+ )
+ pkformat = self.PRIVATE_KEY_FORMAT_ORIGINAL
+ elif keytype == 'OPENSSH':
+ data = self._read_private_key_new_format(
+ lines[start:end],
+ password,
+ )
+ pkformat = self.PRIVATE_KEY_FORMAT_OPENSSH
+ else:
+ raise SSHException(
+ 'encountered {} key, expected {} key'.format(keytype, tag)
+ )
+
+ return pkformat, data
+
+ def _read_private_key_old_format(self, lines, password):
+ start = 0
# parse any headers first
headers = {}
start += 1
while start < len(lines):
- l = lines[start].split(": ")
- if len(l) == 1:
+ line = lines[start].split(": ")
+ if len(line) == 1:
break
- headers[l[0].lower()] = l[1].strip()
+ headers[line[0].lower()] = line[1].strip()
start += 1
- # find end
- end = start
- ending_of_key = "-----END " + tag + " PRIVATE KEY-----"
- while end < len(lines) and lines[end].strip() != ending_of_key:
- end += 1
# if we trudged to the end of the file, just try to cope.
try:
- data = decodebytes(b("".join(lines[start:end])))
+ data = decodebytes(b(''.join(lines[start:])))
except base64.binascii.Error as e:
raise SSHException("base64 decoding error: " + str(e))
if "proc-type" not in headers:
@@ -337,6 +378,132 @@ class PKey(object):
).decryptor()
return decryptor.update(data) + decryptor.finalize()
+ def _read_private_key_new_format(self, lines, password):
+ """
+ Read the new OpenSSH SSH2 private key format available
+ since OpenSSH version 6.5
+ Reference:
+ https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
+ """
+ try:
+ data = decodebytes(b(''.join(lines)))
+ except base64.binascii.Error as e:
+ raise SSHException('base64 decoding error: ' + str(e))
+
+ # read data struct
+ auth_magic = data[:14]
+ if auth_magic != b('openssh-key-v1'):
+ raise SSHException('unexpected OpenSSH key header encountered')
+
+ cstruct = self._uint32_cstruct_unpack(data[15:], 'sssur')
+ cipher, kdfname, kdf_options, num_pubkeys, remainder = cstruct
+ # For now, just support 1 key.
+ if num_pubkeys > 1:
+ raise SSHException(
+ 'unsupported: private keyfile has multiple keys'
+ )
+ pubkey, privkey_blob = self._uint32_cstruct_unpack(remainder, 'ss')
+
+ if kdfname == b('bcrypt'):
+ if cipher == b('aes256-cbc'):
+ mode = modes.CBC
+ elif cipher == b('aes256-ctr'):
+ mode = modes.CTR
+ else:
+ raise SSHException(
+ 'unknown cipher `{}` used in private key file'.format(
+ cipher.decode('utf-8')
+ )
+ )
+ # Encrypted private key.
+ # If no password was passed in, raise an exception pointing
+ # out that we need one
+ if password is None:
+ raise PasswordRequiredException(
+ 'private key file is encrypted'
+ )
+
+ # Unpack salt and rounds from kdfoptions
+ salt, rounds = self._uint32_cstruct_unpack(kdf_options, 'su')
+
+ # run bcrypt kdf to derive key and iv/nonce (32 + 16 bytes)
+ key_iv = bcrypt.kdf(b(password), b(salt), 48, rounds)
+ key = key_iv[:32]
+ iv = key_iv[32:]
+
+ # decrypt private key blob
+ decryptor = Cipher(
+ algorithms.AES(key), mode(iv), default_backend()
+ ).decryptor()
+ decrypted_privkey = decryptor.update(privkey_blob)
+ decrypted_privkey += decryptor.finalize()
+ elif cipher == b('none') and kdfname == b('none'):
+ # Unencrypted private key
+ decrypted_privkey = privkey_blob
+ else:
+ raise SSHException(
+ 'unknown cipher or kdf used in private key file'
+ )
+
+ # Unpack private key and verify checkints
+ cstruct = self._uint32_cstruct_unpack(decrypted_privkey, 'uusr')
+ checkint1, checkint2, keytype, keydata = cstruct
+
+ if checkint1 != checkint2:
+ raise SSHException(
+ 'OpenSSH private key file checkints do not match'
+ )
+
+ # Remove padding
+ padlen = byte_ord(keydata[len(keydata) - 1])
+ return keydata[:len(keydata) - padlen]
+
+ def _uint32_cstruct_unpack(self, data, strformat):
+ """
+ Used to read new OpenSSH private key format.
+ Unpacks a c data structure containing a mix of 32-bit uints and
+ variable length strings prefixed by 32-bit uint size field,
+ according to the specified format. Returns the unpacked vars
+ in a tuple.
+ Format strings:
+ s - denotes a string
+ i - denotes a long integer, encoded as a byte string
+ u - denotes a 32-bit unsigned integer
+ r - the remainder of the input string, returned as a string
+ """
+ arr = []
+ idx = 0
+ try:
+ for f in strformat:
+ if f == 's':
+ # string
+ s_size = struct.unpack('>L', data[idx:idx + 4])[0]
+ idx += 4
+ s = data[idx:idx + s_size]
+ idx += s_size
+ arr.append(s)
+ if f == 'i':
+ # long integer
+ s_size = struct.unpack('>L', data[idx:idx + 4])[0]
+ idx += 4
+ s = data[idx:idx + s_size]
+ idx += s_size
+ i = util.inflate_long(s, True)
+ arr.append(i)
+ elif f == 'u':
+ # 32-bit unsigned int
+ u = struct.unpack('>L', data[idx:idx + 4])[0]
+ idx += 4
+ arr.append(u)
+ elif f == 'r':
+ # remainder as string
+ s = data[idx:]
+ arr.append(s)
+ break
+ except Exception as e:
+ raise SSHException(str(e))
+ return tuple(arr)
+
def _write_private_key_file(self, filename, key, format, password=None):
"""
Write an SSH2-format private key file in a form that can be read by