diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | paramiko/dsskey.py | 15 | ||||
-rw-r--r-- | paramiko/ecdsakey.py | 22 | ||||
-rw-r--r-- | paramiko/ed25519key.py | 4 | ||||
-rw-r--r-- | paramiko/pkey.py | 197 | ||||
-rw-r--r-- | paramiko/rsakey.py | 29 | ||||
-rw-r--r-- | tests/test_dss_1k_o.key | 22 | ||||
-rw-r--r-- | tests/test_pkey.py | 26 | ||||
-rw-r--r-- | tests/test_rsa_2k_o.key | 28 |
9 files changed, 316 insertions, 28 deletions
@@ -9,3 +9,4 @@ demos/*.log _build .coverage .cache +.idea diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index ec358ee2..07feb790 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -229,12 +229,19 @@ class DSSKey(PKey): self._decode_key(data) def _decode_key(self, data): + pkformat, data = data # private key file contains: # DSAPrivateKey = { version = 0, p, q, g, y, x } - try: - keylist = BER(data).decode() - except BERException as e: - raise SSHException("Unable to parse key file: " + str(e)) + if pkformat == self.PRIVATE_KEY_FORMAT_ORIGINAL: + try: + keylist = BER(data).decode() + except BERException as e: + raise SSHException("Unable to parse key file: " + str(e)) + elif pkformat == self.PRIVATE_KEY_FORMAT_OPENSSH: + keylist = self._uint32_cstruct_unpack(data, "iiiii") + keylist = [0] + list(keylist) + else: + raise SSHException("private key format.") if type(keylist) is not list or len(keylist) < 6 or keylist[0] != 0: raise SSHException( "not a valid DSA private key file (bad ber encoding)" diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 353c5f9e..4c84a293 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -283,12 +283,22 @@ class ECDSAKey(PKey): self._decode_key(data) def _decode_key(self, data): - try: - key = serialization.load_der_private_key( - data, password=None, backend=default_backend() - ) - except (ValueError, AssertionError) as e: - raise SSHException(str(e)) + pkformat, data = data + if pkformat == self.PRIVATE_KEY_FORMAT_ORIGINAL: + try: + key = serialization.load_der_private_key( + data, password=None, backend=default_backend() + ) + except (ValueError, AssertionError) as e: + raise SSHException(str(e)) + elif pkformat == self.PRIVATE_KEY_FORMAT_OPENSSH: + curve, verkey, sigkey = self._uint32_cstruct_unpack(data, "sss") + try: + key = ec.derive_private_key(sigkey, curve, default_backend()) + except (AttributeError, TypeError) as e: + raise SSHException(str(e)) + else: + raise SSHException("unknown private key format.") self.signing_key = key self.verifying_key = key.public_key() diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py index 6e0d3ccf..96cff7d0 100644 --- a/paramiko/ed25519key.py +++ b/paramiko/ed25519key.py @@ -75,9 +75,9 @@ class Ed25519Key(PKey): verifying_key = nacl.signing.VerifyKey(msg.get_binary()) elif filename is not None: with open(filename, "r") as f: - data = self._read_private_key("OPENSSH", f) + pkformat, data = self._read_private_key("OPENSSH", f) elif file_obj is not None: - data = self._read_private_key("OPENSSH", file_obj) + pkformat, data = self._read_private_key("OPENSSH", file_obj) if filename or file_obj: signing_key = self._parse_signing_key_data(data, password) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index e37f7751..e7a1cb31 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,14 @@ 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 +73,14 @@ class PKey(object): "mode": modes.CBC, }, } + PRIVATE_KEY_FORMAT_ORIGINAL = 1 + PRIVATE_KEY_FORMAT_OPENSSH = 2 + BEGIN_TAG = re.compile( + r"^-{5}BEGIN (RSA|DSA|EC|OPENSSH) PRIVATE KEY-{5}\s*$" + ) + END_TAG = re.compile( + r"^-{5}END (RSA|DSA|EC|OPENSSH) PRIVATE KEY-{5}\s*$" + ) def __init__(self, msg=None, data=None): """ @@ -281,12 +300,43 @@ 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 - if start >= len(lines): + m = self.BEGIN_TAG.match(lines[start]) + start += 1 + keytype = m.group(1) if m else None + if start >= len(lines) or keytype is None: 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, end, 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, end, password): + start = 0 # parse any headers first headers = {} start += 1 @@ -296,11 +346,6 @@ class PKey(object): break 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]))) @@ -337,6 +382,140 @@ 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, + # We can't control how many rounds are on disk, so no sense + # warning about it. + ignore_few_rounds=True, + ) + 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 diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index 442bfe1f..938660d5 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -180,12 +180,27 @@ class RSAKey(PKey): self._decode_key(data) def _decode_key(self, data): - try: - key = serialization.load_der_private_key( - data, password=None, backend=default_backend() - ) - except ValueError as e: - raise SSHException(str(e)) - + pkformat, data = data + if pkformat == self.PRIVATE_KEY_FORMAT_ORIGINAL: + try: + key = serialization.load_der_private_key( + data, password=None, backend=default_backend() + ) + except ValueError as e: + raise SSHException(str(e)) + elif pkformat == self.PRIVATE_KEY_FORMAT_OPENSSH: + n, e, d, iqmp, q, p = self._uint32_cstruct_unpack(data, "iiiiii") + public_numbers = rsa.RSAPublicNumbers(e=e, n=n) + key = rsa.RSAPrivateNumbers( + p=p, + q=q, + d=d, + dmp1=d % (p - 1), + dmq1=d % (q - 1), + iqmp=iqmp, + public_numbers=public_numbers, + ).private_key(default_backend()) + else: + raise SSHException("unknown private key format.") assert isinstance(key, rsa.RSAPrivateKey) self.key = key diff --git a/tests/test_dss_1k_o.key b/tests/test_dss_1k_o.key new file mode 100644 index 00000000..2a9f8922 --- /dev/null +++ b/tests/test_dss_1k_o.key @@ -0,0 +1,22 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAsyq4pxL +R5sOprPDHGpvzxAAAAEAAAAAEAAAGxAAAAB3NzaC1kc3MAAACBAL8XEx7F9xuwBNles+vW +pNF+YcofrBhjX1r5QhpBe0eoYWLHRcroN6lxwCdGYRfgOoRjTncBiixQX/uUxAY96zDh3i +r492s2BcJt4ihvNn/AY0I0OTuX/2IwGk9CGzafjaeZNVYxMa8lcVt0hSOTjkPQ7gVuk6bJ +zMInvie+VWKLAAAAFQDUgYdY+rhR0SkKbC09BS/SIHcB+wAAAIB44+4zpCNcd0CGvZlowH +99zyPX8uxQtmTLQFuR2O8O0FgVVuCdDgD0D9W8CLOp32oatpM0jyyN89EdvSWzjHzZJ+L6 +H1FtZps7uhpDFWHdva1R25vyGecLMUuXjo5t/D7oCDih+HwHoSAxoi0QvsPd8/qqHQVznN +JKtR6thUpXEwAAAIAG4DCBjbgTTgpBw0egRkJwBSz0oTt+1IcapNU2jA6N8urMSk9YXHEQ +HKN68BAF3YJ59q2Ujv3LOXmBqGd1T+kzwUszfMlgzq8MMu19Yfzse6AIK1Agn1Vj6F7YXL +sXDN+T4KszX5+FJa7t/Zsp3nALWy6l0f4WKivEF5Y2QpEFcQAAAgCH6XUl1hYWB6kgCSHV +a4C+vQHrgFNgNwEQnE074LXHXlAhxC+Dm8XTGqVPX1KRPWzadq9/+v6pqLFqiRueB86uRb +J5WtAbUs3WwxAaC5Mi+mn42MBfL9PIwWPWCvstrAq9Nyj3EBMeX3XFLxN3RuGXIQnY/5rF +f5hriUVxhWDQGIVbBKhkpn7Geqg6nLpn7iqQhzFmFGjPmAdrllgdVGJRLyIN6BRsaltDdy +vxufkvGzKudvQ85QvsaoFJQ6K1d0S7907pexvxmWpcO7zchXb6i09BITWOAKIcHpVkbNQw ++8pzSdpggsAwCRbfk/Jkezz8sXVUCfmmJ23NFUw04/0ZbilCADRsUaPfafgVPeDznBnuCm +tfXa4JSrVUvPdwoex3SKZmYsFXwsuOEQnFkhUGHfWwTbmOmxzy6dtC24KYhnWG5OGFVJXh +3B8jQJGGs2ANfusI/Z0o15tAnQy5fqsLf9TT3RX7RG2ujIiDBsU+A1g//IXmSxxkUOQMZs +v+cMI8KfODAXmQtB30+yAgoV03Zb/bdptv+HqPT4eeecstJUxzEGYADt1mDq3uV7fQbNmo +80bppU52JjztrJb7hBmXsXHPRRK6spQ1FCatqvu1ggZeXZpEifNsHeqCljt87ueXsQsORY +pvhLzjTbTKZmjLDPuB+GxUNLEKh1ZNyAqKng== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 0e60126a..a01acf6f 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -38,6 +38,8 @@ PUB_DSS = "ssh-dss AAAAB3NzaC1kc3MAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgw PUB_ECDSA_256 = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJSPZm3ZWkvk/Zx8WP+fZRZ5/NBBHnGQwR6uIC6XHGPDIHuWUzIjAwA0bzqkOUffEsbLe+uQgKl5kbc/L8KA/eo=" # noqa PUB_ECDSA_384 = "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBBbGibQLW9AAZiGN2hEQxWYYoFaWKwN3PKSaDJSMqmIn1Z9sgRUuw8Y/w502OGvXL/wFk0i2z50l3pWZjD7gfMH7gX5TUiCzwrQkS+Hn1U2S9aF5WJp0NcIzYxXw2r4M2A==" # noqa PUB_ECDSA_521 = "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACaOaFLZGuxa5AW16qj6VLypFbLrEWrt9AZUloCMefxO8bNLjK/O5g0rAVasar1TnyHE9qj4NwzANZASWjQNbc4MAG8vzqezFwLIn/kNyNTsXNfqEko9OgHZknlj2Z79dwTJcRAL4QLcT5aND0EHZLB2fAUDXiWIb2j4rg1mwPlBMiBXA==" # noqa +PUB_RSA_2K_OPENSSH = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDF+Dpr54DX0WdeTDpNAMdkCWEkl3OXtNgf58qlN1gX572OLBqLf0zT4bHstUEpU3piazph/rSWcUMuBoD46tZ6jiH7H9b9Pem2eYQWaELDDkM+v9BMbEy5rMbFRLol5OtEvPFqneyEAanPOgvd8t3yyhSev9QVusakzJ8j8LGgrA8huYZ+Srnw0shEWLG70KUKCh3rG0QIvA8nfhtUOisr2Gp+F0YxMGb5gwBlQYAYE5l6u1SjZ7hNjyNosjK+wRBFgFFBYVpkZKJgWoK9w4ijFyzMZTucnZMqKOKAjIJvHfKBf2/cEfYxSq1EndqTqjYsd9T7/s2vcn1OH5a0wkER" # noqa +PUB_DSS_1K_OPENSSH = "ssh-dss AAAAB3NzaC1kc3MAAACBAL8XEx7F9xuwBNles+vWpNF+YcofrBhjX1r5QhpBe0eoYWLHRcroN6lxwCdGYRfgOoRjTncBiixQX/uUxAY96zDh3ir492s2BcJt4ihvNn/AY0I0OTuX/2IwGk9CGzafjaeZNVYxMa8lcVt0hSOTjkPQ7gVuk6bJzMInvie+VWKLAAAAFQDUgYdY+rhR0SkKbC09BS/SIHcB+wAAAIB44+4zpCNcd0CGvZlowH99zyPX8uxQtmTLQFuR2O8O0FgVVuCdDgD0D9W8CLOp32oatpM0jyyN89EdvSWzjHzZJ+L6H1FtZps7uhpDFWHdva1R25vyGecLMUuXjo5t/D7oCDih+HwHoSAxoi0QvsPd8/qqHQVznNJKtR6thUpXEwAAAIAG4DCBjbgTTgpBw0egRkJwBSz0oTt+1IcapNU2jA6N8urMSk9YXHEQHKN68BAF3YJ59q2Ujv3LOXmBqGd1T+kzwUszfMlgzq8MMu19Yfzse6AIK1Agn1Vj6F7YXLsXDN+T4KszX5+FJa7t/Zsp3nALWy6l0f4WKivEF5Y2QpEFcQ==" # noqa FINGER_RSA = "1024 60:73:38:44:cb:51:86:65:7f:de:da:a2:2b:5a:57:d5" FINGER_DSS = "1024 44:78:f0:b9:a2:3c:c5:18:20:09:ff:75:5b:c1:d2:6c" @@ -45,6 +47,8 @@ FINGER_ECDSA_256 = "256 25:19:eb:55:e6:a1:47:ff:4f:38:d2:75:6f:a5:d5:60" FINGER_ECDSA_384 = "384 c1:8d:a0:59:09:47:41:8e:a8:a6:07:01:29:23:b4:65" FINGER_ECDSA_521 = "521 44:58:22:52:12:33:16:0e:ce:0e:be:2c:7c:7e:cc:1e" SIGNED_RSA = "20:d7:8a:31:21:cb:f7:92:12:f2:a4:89:37:f5:78:af:e6:16:b6:25:b9:97:3d:a2:cd:5f:ca:20:21:73:4c:ad:34:73:8f:20:77:28:e2:94:15:08:d8:91:40:7a:85:83:bf:18:37:95:dc:54:1a:9b:88:29:6c:73:ca:38:b4:04:f1:56:b9:f2:42:9d:52:1b:29:29:b4:4f:fd:c9:2d:af:47:d2:40:76:30:f3:63:45:0c:d9:1d:43:86:0f:1c:70:e2:93:12:34:f3:ac:c5:0a:2f:14:50:66:59:f1:88:ee:c1:4a:e9:d1:9c:4e:46:f0:0e:47:6f:38:74:f1:44:a8" # noqa +FINGER_RSA_2K_OPENSSH = "2048 68:d1:72:01:bf:c0:0c:66:97:78:df:ce:75:74:46:d6" +FINGER_DSS_1K_OPENSSH = "1024 cf:1d:eb:d7:61:d3:12:94:c6:c0:c6:54:35:35:b0:82" RSA_PRIVATE_OUT = """\ -----BEGIN RSA PRIVATE KEY----- @@ -437,6 +441,28 @@ class KeyTest(unittest.TestCase): pub = ECDSAKey(data=key.asbytes()) self.assertTrue(pub.verify_ssh_sig(b"ice weasels", msg)) + def test_22_load_RSA_key_new_format(self): + key = RSAKey.from_private_key_file( + _support("test_rsa_2k_o.key"), b"television" + ) + self.assertEqual("ssh-rsa", key.get_name()) + self.assertEqual(PUB_RSA_2K_OPENSSH.split()[1], key.get_base64()) + self.assertEqual(2048, key.get_bits()) + exp_rsa = b(FINGER_RSA_2K_OPENSSH.split()[1].replace(":", "")) + my_rsa = hexlify(key.get_fingerprint()) + self.assertEqual(exp_rsa, my_rsa) + + def test_23_load_DSS_key_new_format(self): + key = DSSKey.from_private_key_file( + _support("test_dss_1k_o.key"), b"television" + ) + self.assertEqual("ssh-dss", key.get_name()) + self.assertEqual(PUB_DSS_1K_OPENSSH.split()[1], key.get_base64()) + self.assertEqual(1024, key.get_bits()) + exp_rsa = b(FINGER_DSS_1K_OPENSSH.split()[1].replace(":", "")) + my_rsa = hexlify(key.get_fingerprint()) + self.assertEqual(exp_rsa, my_rsa) + def test_salt_size(self): # Read an existing encrypted private key file_ = _support("test_rsa_password.key") diff --git a/tests/test_rsa_2k_o.key b/tests/test_rsa_2k_o.key new file mode 100644 index 00000000..afc15f1c --- /dev/null +++ b/tests/test_rsa_2k_o.key @@ -0,0 +1,28 @@ +-----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABD0R3hOFS + FMb2SJeo5h8QPNAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDF+Dpr54DX + 0WdeTDpNAMdkCWEkl3OXtNgf58qlN1gX572OLBqLf0zT4bHstUEpU3piazph/rSWcUMuBo + D46tZ6jiH7H9b9Pem2eYQWaELDDkM+v9BMbEy5rMbFRLol5OtEvPFqneyEAanPOgvd8t3y + yhSev9QVusakzJ8j8LGgrA8huYZ+Srnw0shEWLG70KUKCh3rG0QIvA8nfhtUOisr2Gp+F0 + YxMGb5gwBlQYAYE5l6u1SjZ7hNjyNosjK+wRBFgFFBYVpkZKJgWoK9w4ijFyzMZTucnZMq + KOKAjIJvHfKBf2/cEfYxSq1EndqTqjYsd9T7/s2vcn1OH5a0wkERAAAD0JnzCJYfDeiUQ6 + 9LOAb6/NnhKvFjCdBYal60MfLcLBHvzHLJvTneQ4f1Vknq8xEVmRba7SDSfwaEybP/1FsP + SGH6FNKA5gKllemgmcaUVr3wtNPtjX4WgsyHcwCRgHmOiyNrUj0OZR5wbZabHIIyirl4wa + LBz8Jb3GalKEagtyWsBKDCKHCFNzh8xmsT1SWhnC7baRyC8e3krQm9hGbNhpj6Q5AtN3ql + wBVamUp0eKxkt70mKBKI4v3DR8KqrEndeK6d0cegVEkE67fqa99a5J3uSDC8mglKrHiKEs + dU1dh/bOF/H3aFpINlRwvlZ95Opby7rG0BHgbZONq0+VUnABVzNTM5Xd5UKjjCF28CrQBf + XS6WeHeUx2zHtOmL1xdePk+Bii+SSUl3pLa4SDwX4nV95cSPx8vMm8dJEruxad6+MPoSuy + Oyho89jqUTSgC/RPejuTgrnB3WbzE5SJb+V3zMata0J1wxbNfYKG9U+VucUZhP4+jzfNqH + B/v8JqtuxnqR8NjPsK2+8wJxebL2KVNjKOm//6P3KSDsavpscGpVWOM06zUlwWCB26W3pP + X/+xO9aR5wiBteFKoJG1waziIjqhOJSmvq+I/texUKEUd/eEFNt10Ubc0zy0sRYVN8rIRJ + masQzCYuUylDzCa4ar1s4qngBZzWL2PRkPuXuhoHuT0J5no174GR6+6EAYZZhnq0tkYrhZ + Ar0tQ4CDlI235a3MPHzvABuwYuWys1tBuLAb+6Gc6CmCiQ+mhojfQUBYG5T65iRFA5UQsH + O1RLEC3yasxGcBI6d0J/fwOP/YLktNu3AeUumr0N9Xgf02DlBNwd+4GOI0LcQvl/3J8ppo + bamTppKPEZ2d32VNEO+Z6Zx5DlIVm5gDeMvIvdwap445VnhL3ZZH2NCkAcXM9+0WH+Quas + JCAMgPYiP9FzF+8Onmj2OmhgIVj/9eanhS3/GLrRC4xCvER2V7PwgB0I5qY110BPEttDyo + IvYE51kvtdW447SK7HZywJnkyw2RNm+29dvWJJwSQckUHuZkXEtmEPk0ePL3yf2NH5XYJc + pXX6Zac0KemCPIHr8l7GogE4Rb2BBTqddkegb9piz6QTAPcQnn+GuMFG06IBhUrgcMEQ8x + UOXYUUrT5HvSxWUcgaYH1nfC3bTWmDaodw8/HQKyF6c44rujO2s2NLFOCAyQMUNdhh3lfD + yHYLO7xYkP6xzzkpk+2lwBoeYdQdAwlKN/XqC8ZhBfwTdem/1hh1BpQJFbbFftWxU8gxxi + iuI+vmlsuIsxKoGCq8YXuophx62lo= + -----END OPENSSH PRIVATE KEY----- |