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