738010
From fc47793e3fedeafdb19e3c3adfbb9c2be82b64c0 Mon Sep 17 00:00:00 2001
8b2250
From: Eduardo Otubo <otubo@redhat.com>
738010
Date: Wed, 6 Mar 2019 14:20:18 +0100
738010
Subject: azure: Filter list of ssh keys pulled from fabric
8b2250
8b2250
RH-Author: Eduardo Otubo <otubo@redhat.com>
738010
Message-id: <20190306142018.8902-1-otubo@redhat.com>
738010
Patchwork-id: 84807
738010
O-Subject: [RHEL-7.7 cloud-init PATCH] azure: Filter list of ssh keys pulled from fabric
738010
Bugzilla: 1684040
8b2250
RH-Acked-by: Cathy Avery <cavery@redhat.com>
8b2250
RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
8b2250
8b2250
From: "Jason Zions (MSFT)" <jasonzio@microsoft.com>
8b2250
8b2250
commit 34f54360fcc1e0f805002a0b639d0a84eb2cb8ee
8b2250
Author: Jason Zions (MSFT) <jasonzio@microsoft.com>
8b2250
Date:   Fri Feb 22 13:26:31 2019 +0000
8b2250
8b2250
    azure: Filter list of ssh keys pulled from fabric
8b2250
8b2250
    The Azure data source is expected to expose a list of
8b2250
    ssh keys for the user-to-be-provisioned in the crawled
8b2250
    metadata. When configured to use the __builtin__ agent
8b2250
    this list is built by the WALinuxAgentShim. The shim
8b2250
    retrieves the full set of certificates and public keys
8b2250
    exposed to the VM from the wireserver, extracts any
8b2250
    ssh keys it can, and returns that list.
8b2250
8b2250
    This fix reduces that list of ssh keys to just the
8b2250
    ones whose fingerprints appear in the "administrative
8b2250
    user" section of the ovf-env.xml file. The Azure
8b2250
    control plane exposes other ssh keys to the VM for
8b2250
    other reasons, but those should not be added to the
8b2250
    authorized_keys file for the provisioned user.
8b2250
8b2250
Signed-off-by: Eduardo Otubo <otubo@redhat.com>
8b2250
Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
8b2250
---
8b2250
 cloudinit/sources/DataSourceAzure.py               |  13 +-
8b2250
 cloudinit/sources/helpers/azure.py                 | 109 ++++++++++-----
8b2250
 tests/data/azure/parse_certificates_fingerprints   |   4 +
8b2250
 tests/data/azure/parse_certificates_pem            | 152 +++++++++++++++++++++
8b2250
 tests/data/azure/pubkey_extract_cert               |  13 ++
8b2250
 tests/data/azure/pubkey_extract_ssh_key            |   1 +
8b2250
 .../unittests/test_datasource/test_azure_helper.py |  71 +++++++++-
8b2250
 7 files changed, 322 insertions(+), 41 deletions(-)
8b2250
 create mode 100644 tests/data/azure/parse_certificates_fingerprints
8b2250
 create mode 100644 tests/data/azure/parse_certificates_pem
8b2250
 create mode 100644 tests/data/azure/pubkey_extract_cert
8b2250
 create mode 100644 tests/data/azure/pubkey_extract_ssh_key
8b2250
8b2250
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
738010
index 7dbeb04..2062ca5 100644
8b2250
--- a/cloudinit/sources/DataSourceAzure.py
8b2250
+++ b/cloudinit/sources/DataSourceAzure.py
738010
@@ -627,9 +627,11 @@ class DataSourceAzure(sources.DataSource):
8b2250
         if self.ds_cfg['agent_command'] == AGENT_START_BUILTIN:
8b2250
             self.bounce_network_with_azure_hostname()
8b2250
 
8b2250
+            pubkey_info = self.cfg.get('_pubkeys', None)
8b2250
             metadata_func = partial(get_metadata_from_fabric,
8b2250
                                     fallback_lease_file=self.
8b2250
-                                    dhclient_lease_file)
8b2250
+                                    dhclient_lease_file,
8b2250
+                                    pubkey_info=pubkey_info)
8b2250
         else:
8b2250
             metadata_func = self.get_metadata_from_agent
8b2250
 
738010
@@ -642,6 +644,7 @@ class DataSourceAzure(sources.DataSource):
8b2250
                 "Error communicating with Azure fabric; You may experience."
8b2250
                 "connectivity issues.", exc_info=True)
8b2250
             return False
8b2250
+
8b2250
         util.del_file(REPORTED_READY_MARKER_FILE)
8b2250
         util.del_file(REPROVISION_MARKER_FILE)
8b2250
         return fabric_data
738010
@@ -909,13 +912,15 @@ def find_child(node, filter_func):
8b2250
 def load_azure_ovf_pubkeys(sshnode):
8b2250
     # This parses a 'SSH' node formatted like below, and returns
8b2250
     # an array of dicts.
8b2250
-    #  [{'fp': '6BE7A7C3C8A8F4B123CCA5D0C2F1BE4CA7B63ED7',
8b2250
-    #    'path': 'where/to/go'}]
8b2250
+    #  [{'fingerprint': '6BE7A7C3C8A8F4B123CCA5D0C2F1BE4CA7B63ED7',
8b2250
+    #    'path': '/where/to/go'}]
8b2250
     #
8b2250
     # <SSH><PublicKeys>
8b2250
-    #   <PublicKey><Fingerprint>ABC</FingerPrint><Path>/ABC</Path>
8b2250
+    #   <PublicKey><Fingerprint>ABC</FingerPrint><Path>/x/y/z</Path>
8b2250
     #   ...
8b2250
     # </PublicKeys></SSH>
8b2250
+    # Under some circumstances, there may be a <Value> element along with the
8b2250
+    # Fingerprint and Path. Pass those along if they appear.
8b2250
     results = find_child(sshnode, lambda n: n.localName == "PublicKeys")
8b2250
     if len(results) == 0:
8b2250
         return []
8b2250
diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py
738010
index e5696b1..2829dd2 100644
8b2250
--- a/cloudinit/sources/helpers/azure.py
8b2250
+++ b/cloudinit/sources/helpers/azure.py
738010
@@ -138,9 +138,36 @@ class OpenSSLManager(object):
8b2250
             self.certificate = certificate
8b2250
         LOG.debug('New certificate generated.')
8b2250
 
8b2250
-    def parse_certificates(self, certificates_xml):
8b2250
-        tag = ElementTree.fromstring(certificates_xml).find(
8b2250
-            './/Data')
8b2250
+    @staticmethod
8b2250
+    def _run_x509_action(action, cert):
8b2250
+        cmd = ['openssl', 'x509', '-noout', action]
8b2250
+        result, _ = util.subp(cmd, data=cert)
8b2250
+        return result
8b2250
+
8b2250
+    def _get_ssh_key_from_cert(self, certificate):
8b2250
+        pub_key = self._run_x509_action('-pubkey', certificate)
8b2250
+        keygen_cmd = ['ssh-keygen', '-i', '-m', 'PKCS8', '-f', '/dev/stdin']
8b2250
+        ssh_key, _ = util.subp(keygen_cmd, data=pub_key)
8b2250
+        return ssh_key
8b2250
+
8b2250
+    def _get_fingerprint_from_cert(self, certificate):
8b2250
+        """openssl x509 formats fingerprints as so:
8b2250
+        'SHA1 Fingerprint=07:3E:19:D1:4D:1C:79:92:24:C6:A0:FD:8D:DA:\
8b2250
+        B6:A8:BF:27:D4:73\n'
8b2250
+
8b2250
+        Azure control plane passes that fingerprint as so:
8b2250
+        '073E19D14D1C799224C6A0FD8DDAB6A8BF27D473'
8b2250
+        """
8b2250
+        raw_fp = self._run_x509_action('-fingerprint', certificate)
8b2250
+        eq = raw_fp.find('=')
8b2250
+        octets = raw_fp[eq+1:-1].split(':')
8b2250
+        return ''.join(octets)
8b2250
+
8b2250
+    def _decrypt_certs_from_xml(self, certificates_xml):
8b2250
+        """Decrypt the certificates XML document using the our private key;
8b2250
+           return the list of certs and private keys contained in the doc.
8b2250
+        """
8b2250
+        tag = ElementTree.fromstring(certificates_xml).find('.//Data')
8b2250
         certificates_content = tag.text
8b2250
         lines = [
8b2250
             b'MIME-Version: 1.0',
738010
@@ -151,32 +178,30 @@ class OpenSSLManager(object):
8b2250
             certificates_content.encode('utf-8'),
8b2250
         ]
8b2250
         with cd(self.tmpdir):
8b2250
-            with open('Certificates.p7m', 'wb') as f:
8b2250
-                f.write(b'\n'.join(lines))
8b2250
             out, _ = util.subp(
8b2250
-                'openssl cms -decrypt -in Certificates.p7m -inkey'
8b2250
+                'openssl cms -decrypt -in /dev/stdin -inkey'
8b2250
                 ' {private_key} -recip {certificate} | openssl pkcs12 -nodes'
8b2250
                 ' -password pass:'.format(**self.certificate_names),
8b2250
-                shell=True)
8b2250
-        private_keys, certificates = [], []
8b2250
+                shell=True, data=b'\n'.join(lines))
8b2250
+        return out
8b2250
+
8b2250
+    def parse_certificates(self, certificates_xml):
8b2250
+        """Given the Certificates XML document, return a dictionary of
8b2250
+           fingerprints and associated SSH keys derived from the certs."""
8b2250
+        out = self._decrypt_certs_from_xml(certificates_xml)
8b2250
         current = []
8b2250
+        keys = {}
8b2250
         for line in out.splitlines():
8b2250
             current.append(line)
8b2250
             if re.match(r'[-]+END .*?KEY[-]+$', line):
8b2250
-                private_keys.append('\n'.join(current))
8b2250
+                # ignore private_keys
8b2250
                 current = []
8b2250
             elif re.match(r'[-]+END .*?CERTIFICATE[-]+$', line):
8b2250
-                certificates.append('\n'.join(current))
8b2250
+                certificate = '\n'.join(current)
8b2250
+                ssh_key = self._get_ssh_key_from_cert(certificate)
8b2250
+                fingerprint = self._get_fingerprint_from_cert(certificate)
8b2250
+                keys[fingerprint] = ssh_key
8b2250
                 current = []
8b2250
-        keys = []
8b2250
-        for certificate in certificates:
8b2250
-            with cd(self.tmpdir):
8b2250
-                public_key, _ = util.subp(
8b2250
-                    'openssl x509 -noout -pubkey |'
8b2250
-                    'ssh-keygen -i -m PKCS8 -f /dev/stdin',
8b2250
-                    data=certificate,
8b2250
-                    shell=True)
8b2250
-            keys.append(public_key)
8b2250
         return keys
8b2250
 
8b2250
 
738010
@@ -206,7 +231,6 @@ class WALinuxAgentShim(object):
8b2250
         self.dhcpoptions = dhcp_options
8b2250
         self._endpoint = None
8b2250
         self.openssl_manager = None
8b2250
-        self.values = {}
8b2250
         self.lease_file = fallback_lease_file
8b2250
 
8b2250
     def clean_up(self):
738010
@@ -328,8 +352,9 @@ class WALinuxAgentShim(object):
8b2250
         LOG.debug('Azure endpoint found at %s', endpoint_ip_address)
8b2250
         return endpoint_ip_address
8b2250
 
8b2250
-    def register_with_azure_and_fetch_data(self):
8b2250
-        self.openssl_manager = OpenSSLManager()
8b2250
+    def register_with_azure_and_fetch_data(self, pubkey_info=None):
8b2250
+        if self.openssl_manager is None:
8b2250
+            self.openssl_manager = OpenSSLManager()
8b2250
         http_client = AzureEndpointHttpClient(self.openssl_manager.certificate)
8b2250
         LOG.info('Registering with Azure...')
8b2250
         attempts = 0
738010
@@ -347,16 +372,37 @@ class WALinuxAgentShim(object):
8b2250
             attempts += 1
8b2250
         LOG.debug('Successfully fetched GoalState XML.')
8b2250
         goal_state = GoalState(response.contents, http_client)
8b2250
-        public_keys = []
8b2250
-        if goal_state.certificates_xml is not None:
8b2250
+        ssh_keys = []
8b2250
+        if goal_state.certificates_xml is not None and pubkey_info is not None:
8b2250
             LOG.debug('Certificate XML found; parsing out public keys.')
8b2250
-            public_keys = self.openssl_manager.parse_certificates(
8b2250
+            keys_by_fingerprint = self.openssl_manager.parse_certificates(
8b2250
                 goal_state.certificates_xml)
8b2250
-        data = {
8b2250
-            'public-keys': public_keys,
8b2250
-        }
8b2250
+            ssh_keys = self._filter_pubkeys(keys_by_fingerprint, pubkey_info)
8b2250
         self._report_ready(goal_state, http_client)
8b2250
-        return data
8b2250
+        return {'public-keys': ssh_keys}
8b2250
+
8b2250
+    def _filter_pubkeys(self, keys_by_fingerprint, pubkey_info):
8b2250
+        """cloud-init expects a straightforward array of keys to be dropped
8b2250
+           into the user's authorized_keys file. Azure control plane exposes
8b2250
+           multiple public keys to the VM via wireserver. Select just the
8b2250
+           user's key(s) and return them, ignoring any other certs.
8b2250
+        """
8b2250
+        keys = []
8b2250
+        for pubkey in pubkey_info:
8b2250
+            if 'value' in pubkey and pubkey['value']:
8b2250
+                keys.append(pubkey['value'])
8b2250
+            elif 'fingerprint' in pubkey and pubkey['fingerprint']:
8b2250
+                fingerprint = pubkey['fingerprint']
8b2250
+                if fingerprint in keys_by_fingerprint:
8b2250
+                    keys.append(keys_by_fingerprint[fingerprint])
8b2250
+                else:
8b2250
+                    LOG.warning("ovf-env.xml specified PublicKey fingerprint "
8b2250
+                                "%s not found in goalstate XML", fingerprint)
8b2250
+            else:
8b2250
+                LOG.warning("ovf-env.xml specified PublicKey with neither "
8b2250
+                            "value nor fingerprint: %s", pubkey)
8b2250
+
8b2250
+        return keys
8b2250
 
8b2250
     def _report_ready(self, goal_state, http_client):
8b2250
         LOG.debug('Reporting ready to Azure fabric.')
738010
@@ -373,11 +419,12 @@ class WALinuxAgentShim(object):
8b2250
         LOG.info('Reported ready to Azure fabric.')
8b2250
 
8b2250
 
8b2250
-def get_metadata_from_fabric(fallback_lease_file=None, dhcp_opts=None):
8b2250
+def get_metadata_from_fabric(fallback_lease_file=None, dhcp_opts=None,
8b2250
+                             pubkey_info=None):
8b2250
     shim = WALinuxAgentShim(fallback_lease_file=fallback_lease_file,
8b2250
                             dhcp_options=dhcp_opts)
8b2250
     try:
8b2250
-        return shim.register_with_azure_and_fetch_data()
8b2250
+        return shim.register_with_azure_and_fetch_data(pubkey_info=pubkey_info)
8b2250
     finally:
8b2250
         shim.clean_up()
8b2250
 
8b2250
diff --git a/tests/data/azure/parse_certificates_fingerprints b/tests/data/azure/parse_certificates_fingerprints
8b2250
new file mode 100644
8b2250
index 0000000..f7293c5
8b2250
--- /dev/null
8b2250
+++ b/tests/data/azure/parse_certificates_fingerprints
8b2250
@@ -0,0 +1,4 @@
8b2250
+ECEDEB3B8488D31AF3BC4CCED493F64B7D27D7B1
8b2250
+073E19D14D1C799224C6A0FD8DDAB6A8BF27D473
8b2250
+4C16E7FAD6297D74A9B25EB8F0A12808CEBE293E
8b2250
+929130695289B450FE45DCD5F6EF0CDE69865867
8b2250
diff --git a/tests/data/azure/parse_certificates_pem b/tests/data/azure/parse_certificates_pem
8b2250
new file mode 100644
8b2250
index 0000000..3521ea3
8b2250
--- /dev/null
8b2250
+++ b/tests/data/azure/parse_certificates_pem
8b2250
@@ -0,0 +1,152 @@
8b2250
+Bag Attributes
8b2250
+    localKeyID: 01 00 00 00
8b2250
+    Microsoft CSP Name: Microsoft Enhanced Cryptographic Provider v1.0
8b2250
+Key Attributes
8b2250
+    X509v3 Key Usage: 10
8b2250
+-----BEGIN PRIVATE KEY-----
8b2250
+MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDlEe5fUqwdrQTP
8b2250
+W2oVlGK2f31q/8ULT8KmOTyUvL0RPdJQ69vvHOc5Q2CKg2eviHC2LWhF8WmpnZj6
8b2250
+61RL0GeFGizwvU8Moebw5p3oqdcgoGpHVtxf+mr4QcWF58/Fwez0dA4hcsimVNBz
8b2250
+eNpBBUIKNBMTBG+4d6hcQBUAGKUdGRcCGEyTqXLU0MgHjxC9JgVqWJl+X2LcAGj5
8b2250
+7J+tGYGTLzKJmeCeGVNN5ZtJ0T85MYHCKQk1/FElK+Kq5akovXffQHjlnCPcx0NJ
8b2250
+47NBjlPaFp2gjnAChn79bT4iCjOFZ9avWpqRpeU517UCnY7djOr3fuod/MSQyh3L
8b2250
+Wuem1tWBAgMBAAECggEBAM4ZXQRs6Kjmo95BHGiAEnSqrlgX+dycjcBq3QPh8KZT
8b2250
+nifqnf48XhnackENy7tWIjr3DctoUq4mOp8AHt77ijhqfaa4XSg7fwKeK9NLBGC5
8b2250
+lAXNtAey0o2894/sKrd+LMkgphoYIUnuI4LRaGV56potkj/ZDP/GwTcG/R4SDnTn
8b2250
+C1Nb05PNTAPQtPZrgPo7TdM6gGsTnFbVrYHQLyg2Sq/osHfF15YohB01esRLCAwb
8b2250
+EF8JkRC4hWIZoV7BsyQ39232zAJQGGla7+wKFs3kObwh3VnFkQpT94KZnNiZuEfG
8b2250
+x5pW4Pn3gXgNsftscXsaNe/M9mYZqo//Qw7NvUIvAvECgYEA9AVveyK0HOA06fhh
8b2250
++3hUWdvw7Pbrl+e06jO9+bT1RjQMbHKyI60DZyVGuAySN86iChJRoJr5c6xj+iXU
8b2250
+cR6BVJDjGH5t1tyiK2aYf6hEpK9/j8Z54UiVQ486zPP0PGfT2TO4lBLK+8AUmoaH
8b2250
+gk21ul8QeVCeCJa/o+xEoRFvzcUCgYEA8FCbbvInrUtNY+9eKaUYoNodsgBVjm5X
8b2250
+I0YPUL9D4d+1nvupHSV2NVmQl0w1RaJwrNTafrl5LkqjhQbmuWNta6QgfZzSA3LB
8b2250
+lWXo1Mm0azKdcD3qMGbvn0Q3zU+yGNEgmB/Yju3/NtgYRG6tc+FCWRbPbiCnZWT8
8b2250
+v3C2Y0XggI0CgYEA2/jCZBgGkTkzue5kNVJlh5OS/aog+pCvL6hxCtarfBuTT3ed
8b2250
+Sje+p46cz3DVpmUpATc+Si8py7KNdYQAm/BJ2be6X+woi9Xcgo87zWgcaPCjZzId
8b2250
+0I2jsIE/Gl6XvpRCDrxnGWRPgt3GNP4szbPLrDPiH9oie8+Y9eYYf7G+PZkCgYEA
8b2250
+nRSzZOPYV4f/QDF4pVQLMykfe/iH9B/fyWjEHg3He19VQmRReIHCMMEoqBziPXAe
8b2250
+onpHj8oAkeer1wpZyhhZr6CKtFDLXgGm09bXSC/IRMHC81klORovyzU2HHfZfCtG
8b2250
+WOmIDnU2+0xpIGIP8sztJ3qnf97MTJSkOSadsWo9gwkCgYEAh5AQmJQmck88Dff2
8b2250
+qIfJIX8d+BDw47BFJ89OmMFjGV8TNB+JO+AV4Vkodg4hxKpLqTFZTTUFgoYfy5u1
8b2250
+1/BhAjpmCDCrzubCFhx+8VEoM2+2+MmnuQoMAm9+/mD/IidwRaARgXgvEmp7sfdt
8b2250
+RyWd+p2lYvFkC/jORQtDMY4uW1o=
8b2250
+-----END PRIVATE KEY-----
8b2250
+Bag Attributes
8b2250
+    localKeyID: 02 00 00 00
8b2250
+    Microsoft CSP Name: Microsoft Strong Cryptographic Provider
8b2250
+Key Attributes
8b2250
+    X509v3 Key Usage: 10
8b2250
+-----BEGIN PRIVATE KEY-----
8b2250
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDlQhPrZwVQYFV4
8b2250
+FBc0H1iTXYaznMpwZvEITKtXWACzTdguUderEVOkXW3HTi5HvC2rMayt0nqo3zcd
8b2250
+x1eGiqdjpZQ/wMrkz9wNEM/nNMsXntEwxk0jCVNKB/jz6vf+BOtrSI01SritAGZW
8b2250
+dpKoTUyztT8C2mA3X6D8g3m4Dd07ltnzxaDqAQIU5jBHh3f/Q14tlPNZWUIiqVTC
8b2250
+gDxgAe7MDmfs9h3CInTBX1XM5J4UsLTL23/padgeSvP5YF5qr1+0c7Tdftxr2lwA
8b2250
+N3rLkisf5EiLAToVyJJlgP/exo2I8DaIKe7DZzD3Y1CrurOpkcMKYu5kM1Htlbua
8b2250
+tDkAa2oDAgMBAAECggEAOvdueS9DyiMlCKAeQb1IQosdQOh0l0ma+FgEABC2CWhd
8b2250
+0LgjQTBRM6cGO+urcq7/jhdWQ1UuUG4tVn71z7itCi/F/Enhxc2C22d2GhFVpWsn
8b2250
+giSXJYpZ/mIjkdVfWNo6FRuRmmHwMys1p0qTOS+8qUJWhSzW75csqJZGgeUrAI61
8b2250
+LBV5F0SGR7dR2xZfy7PeDs9xpD0QivDt5DpsZWPaPvw4QlhdLgw6/YU1h9vtm6ci
8b2250
+xLjnPRLZ7JMpcQHO8dUDl6FiEI7yQ11BDm253VQAVMddYRPQABn7SpEF8kD/aZVh
8b2250
+2Clvz61Rz80SKjPUthMPLWMCRp7zB0xDMzt3/1i+tQKBgQD6Ar1/oD3eFnRnpi4u
8b2250
+n/hdHJtMuXWNfUA4dspNjP6WGOid9sgIeUUdif1XyVJ+afITzvgpWc7nUWIqG2bQ
8b2250
+WxJ/4q2rjUdvjNXTy1voVungR2jD5WLQ9DKeaTR0yCliWlx4JgdPG7qGI5MMwsr+
8b2250
+R/PUoUUhGeEX+o/sCSieO3iUrQKBgQDqwBEMvIdhAv/CK2sG3fsKYX8rFT55ZNX3
8b2250
+Tix9DbUGY3wQColNuI8U1nDlxE9U6VOfT9RPqKelBLCgbzB23kdEJnjSlnqlTxrx
8b2250
+E+Hkndyf2ckdJAR3XNxoQ6SRLJNBsgoBj/z5tlfZE9/Jc+uh0mYy3e6g6XCVPBcz
8b2250
+MgoIc+ofbwKBgQCGQhZ1hR30N+bHCozeaPW9OvGDIE0qcEqeh9xYDRFilXnF6pK9
8b2250
+SjJ9jG7KR8jPLiHb1VebDSl5O1EV/6UU2vNyTc6pw7LLCryBgkGW4aWy1WZDXNnW
8b2250
+EG1meGS9GghvUss5kmJ2bxOZmV0Mi0brisQ8OWagQf+JGvtS7BAt+Q3l+QKBgAb9
8b2250
+8YQPmXiqPjPqVyW9Ntz4SnFeEJ5NApJ7IZgX8GxgSjGwHqbR+HEGchZl4ncE/Bii
8b2250
+qBA3Vcb0fM5KgYcI19aPzsl28fA6ivLjRLcqfIfGVNcpW3iyq13vpdctHLW4N9QU
8b2250
+FdTaOYOds+ysJziKq8CYG6NvUIshXw+HTgUybqbBAoGBAIIOqcmmtgOClAwipA17
8b2250
+dAHsI9Sjk+J0+d4JU6o+5TsmhUfUKIjXf5+xqJkJcQZMEe5GhxcCuYkgFicvh4Hz
8b2250
+kv2H/EU35LcJTqC6KTKZOWIbGcn1cqsvwm3GQJffYDiO8fRZSwCaif2J3F2lfH4Y
8b2250
+R/fA67HXFSTT+OncdRpY1NOn
8b2250
+-----END PRIVATE KEY-----
8b2250
+Bag Attributes: <Empty Attributes>
8b2250
+subject=/CN=CRP/OU=AzureRT/O=Microsoft Corporation/L=Redmond/ST=WA/C=US
8b2250
+issuer=/CN=Root Agency
8b2250
+-----BEGIN CERTIFICATE-----
8b2250
+MIIB+TCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQUFADAWMRQwEgYDVQQDDAtSb290
8b2250
+IEFnZW5jeTAeFw0xOTAyMTUxOTA0MDRaFw0yOTAyMTUxOTE0MDRaMGwxDDAKBgNV
8b2250
+BAMMA0NSUDEQMA4GA1UECwwHQXp1cmVSVDEeMBwGA1UECgwVTWljcm9zb2Z0IENv
8b2250
+cnBvcmF0aW9uMRAwDgYDVQQHDAdSZWRtb25kMQswCQYDVQQIDAJXQTELMAkGA1UE
8b2250
+BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIlPjJXzrRih4C
8b2250
+k/XsoI01oqo7IUxH3dA2F7vHGXQoIpKCp8Qe6Z6cFfdD8Uj+s+B1BX6hngwzIwjN
8b2250
+jE/23X3SALVzJVWzX4Y/IEjbgsuao6sOyNyB18wIU9YzZkVGj68fmMlUw3LnhPbe
8b2250
+eWkufZaJCaLyhQOwlRMbOcn48D6Ys8fccOyXNzpq3rH1OzeQpxS2M8zaJYP4/VZ/
8b2250
+sf6KRpI7bP+QwyFvNKfhcaO9/gj4kMo9lVGjvDU20FW6g8UVNJCV9N4GO6mOcyqo
8b2250
+OhuhVfjCNGgW7N1qi0TIVn0/MQM4l4dcT2R7Z/bV9fhMJLjGsy5A4TLAdRrhKUHT
8b2250
+bzi9HyDvAgMBAAEwDQYJKoZIhvcNAQEFBQADAQA=
8b2250
+-----END CERTIFICATE-----
8b2250
+Bag Attributes
8b2250
+    localKeyID: 01 00 00 00
8b2250
+subject=/C=US/ST=WASHINGTON/L=Seattle/O=Microsoft/OU=Azure/CN=AnhVo/emailAddress=redacted@microsoft.com
8b2250
+issuer=/C=US/ST=WASHINGTON/L=Seattle/O=Microsoft/OU=Azure/CN=AnhVo/emailAddress=redacted@microsoft.com
8b2250
+-----BEGIN CERTIFICATE-----
8b2250
+MIID7TCCAtWgAwIBAgIJALQS3yMg3R41MA0GCSqGSIb3DQEBCwUAMIGMMQswCQYD
8b2250
+VQQGEwJVUzETMBEGA1UECAwKV0FTSElOR1RPTjEQMA4GA1UEBwwHU2VhdHRsZTES
8b2250
+MBAGA1UECgwJTWljcm9zb2Z0MQ4wDAYDVQQLDAVBenVyZTEOMAwGA1UEAwwFQW5o
8b2250
+Vm8xIjAgBgkqhkiG9w0BCQEWE2FuaHZvQG1pY3Jvc29mdC5jb20wHhcNMTkwMjE0
8b2250
+MjMxMjQwWhcNMjExMTEwMjMxMjQwWjCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgM
8b2250
+CldBU0hJTkdUT04xEDAOBgNVBAcMB1NlYXR0bGUxEjAQBgNVBAoMCU1pY3Jvc29m
8b2250
+dDEOMAwGA1UECwwFQXp1cmUxDjAMBgNVBAMMBUFuaFZvMSIwIAYJKoZIhvcNAQkB
8b2250
+FhNhbmh2b0BtaWNyb3NvZnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
8b2250
+CgKCAQEA5RHuX1KsHa0Ez1tqFZRitn99av/FC0/Cpjk8lLy9ET3SUOvb7xznOUNg
8b2250
+ioNnr4hwti1oRfFpqZ2Y+utUS9BnhRos8L1PDKHm8Oad6KnXIKBqR1bcX/pq+EHF
8b2250
+hefPxcHs9HQOIXLIplTQc3jaQQVCCjQTEwRvuHeoXEAVABilHRkXAhhMk6ly1NDI
8b2250
+B48QvSYFaliZfl9i3ABo+eyfrRmBky8yiZngnhlTTeWbSdE/OTGBwikJNfxRJSvi
8b2250
+quWpKL1330B45Zwj3MdDSeOzQY5T2hadoI5wAoZ+/W0+IgozhWfWr1qakaXlOde1
8b2250
+Ap2O3Yzq937qHfzEkMody1rnptbVgQIDAQABo1AwTjAdBgNVHQ4EFgQUPvdgLiv3
8b2250
+pAk4r0QTPZU3PFOZJvgwHwYDVR0jBBgwFoAUPvdgLiv3pAk4r0QTPZU3PFOZJvgw
8b2250
+DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAVUHZT+h9+uCPLTEl5IDg
8b2250
+kqd9WpzXA7PJd/V+7DeDDTkEd06FIKTWZLfxLVVDjQJnQqubQb//e0zGu1qKbXnX
8b2250
+R7xqWabGU4eyPeUFWddmt1OHhxKLU3HbJNJJdL6XKiQtpGGUQt/mqNQ/DEr6hhNF
8b2250
+im5I79iA8H/dXA2gyZrj5Rxea4mtsaYO0mfp1NrFtJpAh2Djy4B1lBXBIv4DWG9e
8b2250
+mMEwzcLCOZj2cOMA6+mdLMUjYCvIRtnn5MKUHyZX5EmX79wsqMTvVpddlVLB9Kgz
8b2250
+Qnvft9+SBWh9+F3ip7BsL6Q4Q9v8eHRbnP0ya7ddlgh64uwf9VOfZZdKCnwqudJP
8b2250
+3g==
8b2250
+-----END CERTIFICATE-----
8b2250
+Bag Attributes
8b2250
+    localKeyID: 02 00 00 00
8b2250
+subject=/CN=/subscriptions/redacted/resourcegroups/redacted/providers/Microsoft.Compute/virtualMachines/redacted
8b2250
+issuer=/CN=Microsoft.ManagedIdentity
8b2250
+-----BEGIN CERTIFICATE-----
8b2250
+MIIDnTCCAoWgAwIBAgIUB2lauSRccvFkoJybUfIwOUqBN7MwDQYJKoZIhvcNAQEL
8b2250
+BQAwJDEiMCAGA1UEAxMZTWljcm9zb2Z0Lk1hbmFnZWRJZGVudGl0eTAeFw0xOTAy
8b2250
+MTUxOTA5MDBaFw0xOTA4MTQxOTA5MDBaMIGUMYGRMIGOBgNVBAMTgYYvc3Vic2Ny
8b2250
+aXB0aW9ucy8yN2I3NTBjZC1lZDQzLTQyZmQtOTA0NC04ZDc1ZTEyNGFlNTUvcmVz
8b2250
+b3VyY2Vncm91cHMvYW5oZXh0cmFzc2gvcHJvdmlkZXJzL01pY3Jvc29mdC5Db21w
8b2250
+dXRlL3ZpcnR1YWxNYWNoaW5lcy9hbmh0ZXN0Y2VydDCCASIwDQYJKoZIhvcNAQEB
8b2250
+BQADggEPADCCAQoCggEBAOVCE+tnBVBgVXgUFzQfWJNdhrOcynBm8QhMq1dYALNN
8b2250
+2C5R16sRU6RdbcdOLke8LasxrK3SeqjfNx3HV4aKp2OllD/AyuTP3A0Qz+c0yxee
8b2250
+0TDGTSMJU0oH+PPq9/4E62tIjTVKuK0AZlZ2kqhNTLO1PwLaYDdfoPyDebgN3TuW
8b2250
+2fPFoOoBAhTmMEeHd/9DXi2U81lZQiKpVMKAPGAB7swOZ+z2HcIidMFfVczknhSw
8b2250
+tMvbf+lp2B5K8/lgXmqvX7RztN1+3GvaXAA3esuSKx/kSIsBOhXIkmWA/97GjYjw
8b2250
+Nogp7sNnMPdjUKu6s6mRwwpi7mQzUe2Vu5q0OQBragMCAwEAAaNWMFQwDgYDVR0P
8b2250
+AQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYD
8b2250
+VR0jBBgwFoAUOJvzEsriQWdJBndPrK+Me1bCPjYwDQYJKoZIhvcNAQELBQADggEB
8b2250
+AFGP/g8o7Hv/to11M0UqfzJuW/AyH9RZtSRcNQFLZUndwweQ6fap8lFsA4REUdqe
8b2250
+7Quqp5JNNY1XzKLWXMPoheIDH1A8FFXdsAroArzlNs9tO3TlIHE8A7HxEVZEmR4b
8b2250
+7ZiixmkQPS2RkjEoV/GM6fheBrzuFn7X5kVZyE6cC5sfcebn8xhk3ZcXI0VmpdT0
8b2250
+jFBsf5IvFCIXXLLhJI4KXc8VMoKFU1jT9na/jyaoGmfwovKj4ib8s2aiXGAp7Y38
8b2250
+UCmY+bJapWom6Piy5Jzi/p/kzMVdJcSa+GqpuFxBoQYEVs2XYVl7cGu/wPM+NToC
8b2250
+pkSoWwF1QAnHn0eokR9E1rU=
8b2250
+-----END CERTIFICATE-----
8b2250
+Bag Attributes: <Empty Attributes>
8b2250
+subject=/CN=CRP/OU=AzureRT/O=Microsoft Corporation/L=Redmond/ST=WA/C=US
8b2250
+issuer=/CN=Root Agency
8b2250
+-----BEGIN CERTIFICATE-----
8b2250
+MIIB+TCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQUFADAWMRQwEgYDVQQDDAtSb290
8b2250
+IEFnZW5jeTAeFw0xOTAyMTUxOTA0MDRaFw0yOTAyMTUxOTE0MDRaMGwxDDAKBgNV
8b2250
+BAMMA0NSUDEQMA4GA1UECwwHQXp1cmVSVDEeMBwGA1UECgwVTWljcm9zb2Z0IENv
8b2250
+cnBvcmF0aW9uMRAwDgYDVQQHDAdSZWRtb25kMQswCQYDVQQIDAJXQTELMAkGA1UE
8b2250
+BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHU9IDclbKVYVb
8b2250
+Yuv0+zViX+wTwlKspslmy/uf3hkWLh7pyzyrq70S7qtSW2EGixUPxZS/R8pOLHoi
8b2250
+nlKF9ILgj0gVTCJsSwnWpXRg3rhZwIVoYMHN50BHS1SqVD0lsWNMXmo76LoJcjmW
8b2250
+vwIznvj5C/gnhU+K7+c3m7AlCyU2wjwpBAEYj7PQs6l/wTqpEiaqC5NytNBd7qp+
8b2250
+lYYysVrpa1PFL0Nj4MMZARIfjkiJtL9qDhy9YZeJRQ6q/Fhz0kjvkZnfxixfKF4y
8b2250
+WzOfhBrAtpF6oOnuYKk3hxjh9KjTTX4/U8zdLojalX09iyHyEjwJKGlGEpzh1aY7
8b2250
+t5btUyvpAgMBAAEwDQYJKoZIhvcNAQEFBQADAQA=
8b2250
+-----END CERTIFICATE-----
8b2250
diff --git a/tests/data/azure/pubkey_extract_cert b/tests/data/azure/pubkey_extract_cert
8b2250
new file mode 100644
8b2250
index 0000000..ce9b852
8b2250
--- /dev/null
8b2250
+++ b/tests/data/azure/pubkey_extract_cert
8b2250
@@ -0,0 +1,13 @@
8b2250
+-----BEGIN CERTIFICATE-----
8b2250
+MIIB+TCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQUFADAWMRQwEgYDVQQDDAtSb290
8b2250
+IEFnZW5jeTAeFw0xOTAyMTUxOTA0MDRaFw0yOTAyMTUxOTE0MDRaMGwxDDAKBgNV
8b2250
+BAMMA0NSUDEQMA4GA1UECwwHQXp1cmVSVDEeMBwGA1UECgwVTWljcm9zb2Z0IENv
8b2250
+cnBvcmF0aW9uMRAwDgYDVQQHDAdSZWRtb25kMQswCQYDVQQIDAJXQTELMAkGA1UE
8b2250
+BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHU9IDclbKVYVb
8b2250
+Yuv0+zViX+wTwlKspslmy/uf3hkWLh7pyzyrq70S7qtSW2EGixUPxZS/R8pOLHoi
8b2250
+nlKF9ILgj0gVTCJsSwnWpXRg3rhZwIVoYMHN50BHS1SqVD0lsWNMXmo76LoJcjmW
8b2250
+vwIznvj5C/gnhU+K7+c3m7AlCyU2wjwpBAEYj7PQs6l/wTqpEiaqC5NytNBd7qp+
8b2250
+lYYysVrpa1PFL0Nj4MMZARIfjkiJtL9qDhy9YZeJRQ6q/Fhz0kjvkZnfxixfKF4y
8b2250
+WzOfhBrAtpF6oOnuYKk3hxjh9KjTTX4/U8zdLojalX09iyHyEjwJKGlGEpzh1aY7
8b2250
+t5btUyvpAgMBAAEwDQYJKoZIhvcNAQEFBQADAQA=
8b2250
+-----END CERTIFICATE-----
8b2250
diff --git a/tests/data/azure/pubkey_extract_ssh_key b/tests/data/azure/pubkey_extract_ssh_key
8b2250
new file mode 100644
8b2250
index 0000000..54d749e
8b2250
--- /dev/null
8b2250
+++ b/tests/data/azure/pubkey_extract_ssh_key
8b2250
@@ -0,0 +1 @@
8b2250
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHU9IDclbKVYVbYuv0+zViX+wTwlKspslmy/uf3hkWLh7pyzyrq70S7qtSW2EGixUPxZS/R8pOLHoinlKF9ILgj0gVTCJsSwnWpXRg3rhZwIVoYMHN50BHS1SqVD0lsWNMXmo76LoJcjmWvwIznvj5C/gnhU+K7+c3m7AlCyU2wjwpBAEYj7PQs6l/wTqpEiaqC5NytNBd7qp+lYYysVrpa1PFL0Nj4MMZARIfjkiJtL9qDhy9YZeJRQ6q/Fhz0kjvkZnfxixfKF4yWzOfhBrAtpF6oOnuYKk3hxjh9KjTTX4/U8zdLojalX09iyHyEjwJKGlGEpzh1aY7t5btUyvp
8b2250
diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py
738010
index 26b2b93..0255616 100644
8b2250
--- a/tests/unittests/test_datasource/test_azure_helper.py
8b2250
+++ b/tests/unittests/test_datasource/test_azure_helper.py
8b2250
@@ -1,11 +1,13 @@
8b2250
 # This file is part of cloud-init. See LICENSE file for license information.
8b2250
 
8b2250
 import os
8b2250
+import unittest2
8b2250
 from textwrap import dedent
8b2250
 
8b2250
 from cloudinit.sources.helpers import azure as azure_helper
8b2250
 from cloudinit.tests.helpers import CiTestCase, ExitStack, mock, populate_dir
8b2250
 
8b2250
+from cloudinit.util import load_file
8b2250
 from cloudinit.sources.helpers.azure import WALinuxAgentShim as wa_shim
8b2250
 
8b2250
 GOAL_STATE_TEMPLATE = """\
738010
@@ -289,6 +291,50 @@ class TestOpenSSLManager(CiTestCase):
8b2250
         self.assertEqual([mock.call(manager.tmpdir)], del_dir.call_args_list)
8b2250
 
8b2250
 
8b2250
+class TestOpenSSLManagerActions(CiTestCase):
8b2250
+
8b2250
+    def setUp(self):
8b2250
+        super(TestOpenSSLManagerActions, self).setUp()
8b2250
+
8b2250
+        self.allowed_subp = True
8b2250
+
8b2250
+    def _data_file(self, name):
8b2250
+        path = 'tests/data/azure'
8b2250
+        return os.path.join(path, name)
8b2250
+
8b2250
+    @unittest2.skip("todo move to cloud_test")
8b2250
+    def test_pubkey_extract(self):
8b2250
+        cert = load_file(self._data_file('pubkey_extract_cert'))
8b2250
+        good_key = load_file(self._data_file('pubkey_extract_ssh_key'))
8b2250
+        sslmgr = azure_helper.OpenSSLManager()
8b2250
+        key = sslmgr._get_ssh_key_from_cert(cert)
8b2250
+        self.assertEqual(good_key, key)
8b2250
+
8b2250
+        good_fingerprint = '073E19D14D1C799224C6A0FD8DDAB6A8BF27D473'
8b2250
+        fingerprint = sslmgr._get_fingerprint_from_cert(cert)
8b2250
+        self.assertEqual(good_fingerprint, fingerprint)
8b2250
+
8b2250
+    @unittest2.skip("todo move to cloud_test")
8b2250
+    @mock.patch.object(azure_helper.OpenSSLManager, '_decrypt_certs_from_xml')
8b2250
+    def test_parse_certificates(self, mock_decrypt_certs):
8b2250
+        """Azure control plane puts private keys as well as certificates
8b2250
+           into the Certificates XML object. Make sure only the public keys
8b2250
+           from certs are extracted and that fingerprints are converted to
8b2250
+           the form specified in the ovf-env.xml file.
8b2250
+        """
8b2250
+        cert_contents = load_file(self._data_file('parse_certificates_pem'))
8b2250
+        fingerprints = load_file(self._data_file(
8b2250
+            'parse_certificates_fingerprints')
8b2250
+        ).splitlines()
8b2250
+        mock_decrypt_certs.return_value = cert_contents
8b2250
+        sslmgr = azure_helper.OpenSSLManager()
8b2250
+        keys_by_fp = sslmgr.parse_certificates('')
8b2250
+        for fp in keys_by_fp.keys():
8b2250
+            self.assertIn(fp, fingerprints)
8b2250
+        for fp in fingerprints:
8b2250
+            self.assertIn(fp, keys_by_fp)
8b2250
+
8b2250
+
8b2250
 class TestWALinuxAgentShim(CiTestCase):
8b2250
 
8b2250
     def setUp(self):
738010
@@ -329,18 +375,31 @@ class TestWALinuxAgentShim(CiTestCase):
8b2250
 
8b2250
     def test_certificates_used_to_determine_public_keys(self):
8b2250
         shim = wa_shim()
8b2250
-        data = shim.register_with_azure_and_fetch_data()
8b2250
+        """if register_with_azure_and_fetch_data() isn't passed some info about
8b2250
+           the user's public keys, there's no point in even trying to parse
8b2250
+           the certificates
8b2250
+        """
8b2250
+        mypk = [{'fingerprint': 'fp1', 'path': 'path1'},
8b2250
+                {'fingerprint': 'fp3', 'path': 'path3', 'value': ''}]
8b2250
+        certs = {'fp1': 'expected-key',
8b2250
+                 'fp2': 'should-not-be-found',
8b2250
+                 'fp3': 'expected-no-value-key',
8b2250
+                 }
8b2250
+        sslmgr = self.OpenSSLManager.return_value
8b2250
+        sslmgr.parse_certificates.return_value = certs
8b2250
+        data = shim.register_with_azure_and_fetch_data(pubkey_info=mypk)
8b2250
         self.assertEqual(
8b2250
             [mock.call(self.GoalState.return_value.certificates_xml)],
8b2250
-            self.OpenSSLManager.return_value.parse_certificates.call_args_list)
8b2250
-        self.assertEqual(
8b2250
-            self.OpenSSLManager.return_value.parse_certificates.return_value,
8b2250
-            data['public-keys'])
8b2250
+            sslmgr.parse_certificates.call_args_list)
8b2250
+        self.assertIn('expected-key', data['public-keys'])
8b2250
+        self.assertIn('expected-no-value-key', data['public-keys'])
8b2250
+        self.assertNotIn('should-not-be-found', data['public-keys'])
8b2250
 
8b2250
     def test_absent_certificates_produces_empty_public_keys(self):
8b2250
+        mypk = [{'fingerprint': 'fp1', 'path': 'path1'}]
8b2250
         self.GoalState.return_value.certificates_xml = None
8b2250
         shim = wa_shim()
8b2250
-        data = shim.register_with_azure_and_fetch_data()
8b2250
+        data = shim.register_with_azure_and_fetch_data(pubkey_info=mypk)
8b2250
         self.assertEqual([], data['public-keys'])
8b2250
 
8b2250
     def test_correct_url_used_for_report_ready(self):
8b2250
-- 
8b2250
1.8.3.1
8b2250