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