Repository: Azure/WALinuxAgent
Pull request #750 (2017-06-07 11:52:24)

GAiA OS: Fix SSH public key handling

msftclas: 
@chkp-zegman,
Thanks for having already signed the Contribution License Agreement. Your agreement was validated by Microsoft. We will now review your pull request.
_Thanks,
Microsoft Pull Request Bot_

Patch:
diff --git a/azurelinuxagent/common/osutil/gaia.py b/azurelinuxagent/common/osutil/gaia.py
index a1069d33..85a59aa7 100644
--- a/azurelinuxagent/common/osutil/gaia.py
+++ b/azurelinuxagent/common/osutil/gaia.py
@@ -16,15 +16,20 @@
 # Requires Python 2.4+ and Openssl 1.0+
 #
 
+import base64
 import socket
 import struct
 import time
 
-import azurelinuxagent.common.logger as logger
+import azurelinuxagent.common.conf as conf
 from azurelinuxagent.common.exception import OSUtilError
+from azurelinuxagent.common.future import ustr, bytebuffer
+import azurelinuxagent.common.logger as logger
+from azurelinuxagent.common.osutil.default import DefaultOSUtil
+from azurelinuxagent.common.utils.cryptutil import CryptUtil
+import azurelinuxagent.common.utils.fileutil as fileutil
 import azurelinuxagent.common.utils.shellutil as shellutil
 import azurelinuxagent.common.utils.textutil as textutil
-from azurelinuxagent.common.osutil.default import DefaultOSUtil
 
 
 class GaiaOSUtil(DefaultOSUtil):
@@ -64,12 +69,11 @@ class GaiaOSUtil(DefaultOSUtil):
         if ret != 0:
             raise OSUtilError("Failed to delete root password")
 
-    def _replace_user(path, username):
+    def _replace_user(self, path, username):
+        if path.startswith('$HOME'):
+            path = '/home' + path[5:]
         parts = path.split('/')
-        for i in xrange(len(parts)):
-            if parts[i] == '$HOME':
-                parts[i + 1] = username
-                break
+        parts[2] = username
         return '/'.join(parts)
 
     def deploy_ssh_keypair(self, username, keypair):
@@ -80,13 +84,57 @@ class GaiaOSUtil(DefaultOSUtil):
         super(GaiaOSUtil, self).deploy_ssh_keypair(
             username, (path, thumbprint))
 
+    def openssl_to_openssh(self, input_file, output_file):
+        cryptutil = CryptUtil(conf.get_openssl_cmd())
+        ret, out = shellutil.run_get_output(
+            conf.get_openssl_cmd() +
+            " rsa -pubin -noout -text -in '" + input_file + "'")
+        if ret != 0:
+            raise OSUtilError('openssl failed with {0}'.format(ret))
+
+        modulus = []
+        exponent = []
+        buf = None
+        for line in out.split('\n'):
+            if line.startswith('Modulus:'):
+                buf = modulus
+                buf.append(line)
+                continue
+            if line.startswith('Exponent:'):
+                buf = exponent
+                buf.append(line)
+                continue
+            if buf and line:
+                buf.append(line.strip().replace(':', ''))
+
+        def text_to_num(buf):
+            if len(buf) == 1:
+                return int(buf[0].split()[1])
+            return long(''.join(buf[1:]), 16)
+
+        n = text_to_num(modulus)
+        e = text_to_num(exponent)
+
+        keydata = bytearray()
+        keydata.extend(struct.pack('>I', len('ssh-rsa')))
+        keydata.extend(b'ssh-rsa')
+        keydata.extend(struct.pack('>I', len(cryptutil.num_to_bytes(e))))
+        keydata.extend(cryptutil.num_to_bytes(e))
+        keydata.extend(struct.pack('>I', len(cryptutil.num_to_bytes(n)) + 1))
+        keydata.extend(b'\0')
+        keydata.extend(cryptutil.num_to_bytes(n))
+        keydata_base64 = base64.b64encode(bytebuffer(keydata))
+        fileutil.write_file(output_file,
+                            ustr(b'ssh-rsa ' + keydata_base64 + b'\n',
+                                 encoding='utf-8'))
+
     def deploy_ssh_pubkey(self, username, pubkey):
         logger.info('deploy_ssh_pubkey')
         username = 'admin'
         path, thumbprint, value = pubkey
         path = self._replace_user(path, username)
         super(GaiaOSUtil, self).deploy_ssh_pubkey(
-            'admin', (path, thumbprint, value))
+            username, (path, thumbprint, value))
 
     def eject_dvd(self, chk_err=True):
         logger.warn('eject is not supported on GAiA')
@@ -114,7 +162,7 @@ class GaiaOSUtil(DefaultOSUtil):
     def restart_ssh_service(self):
         return shellutil.run('/sbin/service sshd condrestart', chk_err=False)
 
-    def _address_to_string(addr):
+    def _address_to_string(self, addr):
         return socket.inet_ntoa(struct.pack("!I", addr))
 
     def _get_prefix(self, mask):
diff --git a/azurelinuxagent/common/protocol/hostplugin.py b/azurelinuxagent/common/protocol/hostplugin.py
index 464fd354..bf62cb54 100644
--- a/azurelinuxagent/common/protocol/hostplugin.py
+++ b/azurelinuxagent/common/protocol/hostplugin.py
@@ -288,12 +288,23 @@ class HostPluginProtocol(object):
     
     @staticmethod
     def read_response_error(response):
-        if response is None:
-            return ''
-        body = remove_bom(response.read())
-        if PY_VERSION_MAJOR < 3 and body is not None:
-            body = ustr(body, encoding='utf-8')
-        return "{0}, {1}, {2}".format(
-            response.status,
-            response.reason,
-            body)
+        result = ''
+        if response is not None:
+            try:
+                body = remove_bom(response.read())
+                result = "[{0}: {1}] {2}".format(response.status,
+                                                 response.reason,
+                                                 body)
+
+                # this result string is passed upstream to several methods
+                # which do a raise HttpError() or a format() of some kind;
+                # as a result it cannot have any unicode characters
+                if PY_VERSION_MAJOR < 3:
+                    result = ustr(result, encoding='ascii', errors='ignore')
+                else:
+                    result = result\
+                        .encode(encoding='ascii', errors='ignore')\
+                        .decode(encoding='ascii', errors='ignore')
+            except Exception:
+                logger.warn(traceback.format_exc())
+        return result
diff --git a/config/gaia/waagent.conf b/config/gaia/waagent.conf
index 43ad35d7..75550a67 100644
--- a/config/gaia/waagent.conf
+++ b/config/gaia/waagent.conf
@@ -55,7 +55,7 @@ ResourceDisk.SwapSizeMB=1024
 ResourceDisk.MountOptions=None
 
 # Enable verbose logging (y|n)
-Logs.Verbose=y
+Logs.Verbose=n
 
 # Is FIPS enabled
 OS.EnableFIPS=n


Test patch:
diff --git a/tests/protocol/test_hostplugin.py b/tests/protocol/test_hostplugin.py
index e203615a..b18b6914 100644
--- a/tests/protocol/test_hostplugin.py
+++ b/tests/protocol/test_hostplugin.py
@@ -19,7 +19,7 @@ import base64
 import json
 import sys
 
-
+from azurelinuxagent.common.future import ustr
 
 if sys.version_info[0] == 3:
     import http.client as httpclient
@@ -224,6 +224,61 @@ class TestHostPlugin(AgentTestCase):
                     test_goal_state,
                     exp_method, exp_url, exp_data)
 
+    def test_read_response_error(self):
+        """
+        Validate the read_response_error method handles encoding correctly
+        """
+        responses = ['message', b'message', '\x80message\x80']
+        response = MagicMock()
+        response.status = 'status'
+        response.reason = 'reason'
+        with patch.object(response, 'read') as patch_response:
+            for s in responses:
+                patch_response.return_value = s
+                result = hostplugin.HostPluginProtocol.read_response_error(response)
+                self.assertTrue('[status: reason]' in result)
+                self.assertTrue('message' in result)
+
+    def test_read_response_bytes(self):
+        response_bytes = '7b:0a:20:20:20:20:22:65:72:72:6f:72:43:6f:64:65:22:' \
+                         '3a:20:22:54:68:65:20:62:6c:6f:62:20:74:79:70:65:20:' \
+                         '69:73:20:69:6e:76:61:6c:69:64:20:66:6f:72:20:74:68:' \
+                         '69:73:20:6f:70:65:72:61:74:69:6f:6e:2e:22:2c:0a:20:' \
+                         '20:20:20:22:6d:65:73:73:61:67:65:22:3a:20:22:c3:af:' \
+                         'c2:bb:c2:bf:3c:3f:78:6d:6c:20:76:65:72:73:69:6f:6e:' \
+                         '3d:22:31:2e:30:22:20:65:6e:63:6f:64:69:6e:67:3d:22:' \
+                         '75:74:66:2d:38:22:3f:3e:3c:45:72:72:6f:72:3e:3c:43:' \
+                         '6f:64:65:3e:49:6e:76:61:6c:69:64:42:6c:6f:62:54:79:' \
+                         '70:65:3c:2f:43:6f:64:65:3e:3c:4d:65:73:73:61:67:65:' \
+                         '3e:54:68:65:20:62:6c:6f:62:20:74:79:70:65:20:69:73:' \
+                         '20:69:6e:76:61:6c:69:64:20:66:6f:72:20:74:68:69:73:' \
+                         '20:6f:70:65:72:61:74:69:6f:6e:2e:0a:52:65:71:75:65:' \
+                         '73:74:49:64:3a:63:37:34:32:39:30:63:62:2d:30:30:30:' \
+                         '31:2d:30:30:62:35:2d:30:36:64:61:2d:64:64:36:36:36:' \
+                         '61:30:30:30:22:2c:0a:20:20:20:20:22:64:65:74:61:69:' \
+                         '6c:73:22:3a:20:22:22:0a:7d'.split(':')
+        expected_response = '[status: reason] {\n    "errorCode": "The blob ' \
+                            'type is invalid for this operation.",\n    ' \
+                            '"message": "<?xml version="1.0" ' \
+                            'encoding="utf-8"?>' \
+                            '<Error><Code>InvalidBlobType</Code><Message>The ' \
+                            'blob type is invalid for this operation.\n' \
+                            'RequestId:c74290cb-0001-00b5-06da-dd666a000",' \
+                            '\n    "details": ""\n}'
+
+        response_string = ''.join(chr(int(b, 16)) for b in response_bytes)
+        response = MagicMock()
+        response.status = 'status'
+        response.reason = 'reason'
+        with patch.object(response, 'read') as patch_response:
+            patch_response.return_value = response_string
+            result = hostplugin.HostPluginProtocol.read_response_error(response)
+            self.assertEqual(result, expected_response)
+            try:
+                raise HttpError("{0}".format(result))
+            except HttpError as e:
+                self.assertTrue(result in ustr(e))
+
     def test_no_fallback(self):
         """
         Validate fallback to upload status using HostGAPlugin is not happening