diff --git a/cloudbaseinit/plugins/windows/winrmcertificateauth.py b/cloudbaseinit/plugins/windows/winrmcertificateauth.py new file mode 100644 index 00000000..fd7f9273 --- /dev/null +++ b/cloudbaseinit/plugins/windows/winrmcertificateauth.py @@ -0,0 +1,88 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the 'License'); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cloudbaseinit.openstack.common import log as logging +from cloudbaseinit.plugins import base +from cloudbaseinit.plugins import constants +from cloudbaseinit.plugins.windows import x509 +from cloudbaseinit.plugins.windows import winrmconfig + +LOG = logging.getLogger(__name__) + + +class ConfigWinRMCertificateAuthPlugin(base.BasePlugin): + def _get_client_auth_cert(self, service): + meta_data = service.get_meta_data('openstack') + meta = meta_data.get('meta') + if meta: + i = 0 + cert_data = "" + while True: + # Chunking is necessary as metadata items can be + # max. 255 chars long + cert_chunk = meta.get('admin_cert%d' % i) + if not cert_chunk: + break + cert_data += cert_chunk + i += 1 + + return cert_data + + def _get_credentials(self, shared_data): + user_name = shared_data.get(constants.SHARED_DATA_USERNAME) + if not user_name: + raise Exception("Cannot execute plugin as the username has " + "not been set in the plugins shared data") + + password = shared_data.get(constants.SHARED_DATA_PASSWORD) + if not password: + raise Exception("Cannot execute plugin as the password has " + "not been set in the plugins shared data") + + # For security reasons unset the password in the shared_data + # as it is currently not needed by other plugins + shared_data[constants.SHARED_DATA_PASSWORD] = None + + return (user_name, password) + + def execute(self, service, shared_data): + user_name, password = self._get_credentials(shared_data) + + cert_data = self._get_client_auth_cert(service) + if not cert_data: + LOG.info("WinRM certificate authentication cannot be configured " + "as a certificate has not been provided in the metadata") + return (base.PLUGIN_EXECUTION_DONE, False) + + cert_manager = x509.CryptoAPICertManager() + cert_thumprint, cert_upn = cert_manager.import_cert( + cert_data, store_name=x509.STORE_NAME_ROOT) + + if not cert_upn: + LOG.error("WinRM certificate authentication cannot be configured " + "as the provided certificate lacks a subject alt name " + "containing an UPN (OID 1.3.6.1.4.1.311.20.2.3)") + return (base.PLUGIN_EXECUTION_DONE, False) + + winrm_config = winrmconfig.WinRMConfig() + winrm_config.set_auth_config(certificate=True) + + if winrm_config.get_cert_mapping(cert_thumprint, cert_upn): + winrm_config.delete_cert_mapping(cert_thumprint, cert_upn) + winrm_config.create_cert_mapping(cert_thumprint, cert_upn, + user_name, password) + + return (base.PLUGIN_EXECUTION_DONE, False) diff --git a/cloudbaseinit/plugins/windows/winrmconfig.py b/cloudbaseinit/plugins/windows/winrmconfig.py index e109e76e..6f8ad81a 100644 --- a/cloudbaseinit/plugins/windows/winrmconfig.py +++ b/cloudbaseinit/plugins/windows/winrmconfig.py @@ -31,7 +31,10 @@ LISTENER_PROTOCOL_HTTPS = "HTTPS" class WinRMConfig(object): _SERVICE_AUTH_URI = 'winrm/Config/Service/Auth' - _SERVICE_LISTENER_URI = 'winrm/Config/Listener?Address=*+Transport=%s' + _SERVICE_LISTENER_URI = ('winrm/Config/Listener?Address=' + '%(address)s+Transport=%(protocol)s') + _SERVICE_CERTMAPPING_URI = ('winrm/Config/Service/certmapping?Issuer=' + '%(issuer)s+Subject=%(subject)s+Uri=%(uri)s') def _get_wsman_session(self): wsman = client.Dispatch('WSMan.Automation') @@ -41,6 +44,9 @@ class WinRMConfig(object): return re.match("^{.*}(.*)$", tag).groups(1)[0] def _parse_listener_xml(self, data_xml): + if not data_xml: + return None + listening_on = [] data = {"ListeningOn": listening_on} @@ -64,50 +70,110 @@ class WinRMConfig(object): return data - def get_listener(self, protocol=LISTENER_PROTOCOL_HTTPS): + def _parse_cert_mapping_xml(self, data_xml): + if not data_xml: + return None + + data = {} + + ns = {'cfg': + 'http://schemas.microsoft.com/wbem/wsman/1/config/service/' + 'certmapping.xsd'} + tree = ElementTree.fromstring(data_xml) + for node in tree: + tag = self._get_node_tag(node.tag) + if tag == "Enabled": + if node.text == "true": + value = True + else: + value = False + data[tag] = value + else: + data[tag] = node.text + + return data + + def _get_xml_bool(self, value): + if value: + return "true" + else: + return "false" + + def _get_resource(self, resource_uri): session = self._get_wsman_session() - resourceUri = self._SERVICE_LISTENER_URI % protocol try: - data_xml = session.Get(resourceUri) + return session.Get(resource_uri) except pywintypes.com_error, ex: if len(ex.excepinfo) > 5 and ex.excepinfo[5] == -2144108544: return None else: raise - return self._parse_listener_xml(data_xml) - - def delete_listener(self, protocol=LISTENER_PROTOCOL_HTTPS): + def _delete_resource(self, resource_uri): session = self._get_wsman_session() - resourceUri = self._SERVICE_LISTENER_URI % protocol - session.Delete(resourceUri) + session.Delete(resource_uri) - def create_listener(self, protocol=LISTENER_PROTOCOL_HTTPS, enabled=True, - cert_thumbprint=None): + def _create_resource(self, resource_uri, data_xml): session = self._get_wsman_session() - resource_uri = self._SERVICE_LISTENER_URI % protocol + session.Create(resource_uri, data_xml) - if enabled: - enabled_str = "true" - else: - enabled_str = "false" + def get_cert_mapping(self, issuer, subject, uri="*"): + resource_uri = self._SERVICE_CERTMAPPING_URI % {'issuer': issuer, + 'subject': subject, + 'uri': uri} + return self._parse_cert_mapping_xml(self._get_resource(resource_uri)) - session.Create( + def delete_cert_mapping(self, issuer, subject, uri="*"): + resource_uri = self._SERVICE_CERTMAPPING_URI % {'issuer': issuer, + 'subject': subject, + 'uri': uri} + self._delete_resource(resource_uri) + + def create_cert_mapping(self, issuer, subject, username, password, + uri="*", enabled=True): + resource_uri = self._SERVICE_CERTMAPPING_URI % {'issuer': issuer, + 'subject': subject, + 'uri': uri} + self._create_resource( + resource_uri, + '' + '%(enabled)s' + '%(password)s' + '%(username)s' + '' % {'enabled': self._get_xml_bool(enabled), + 'username': username, + 'password': password}) + + def get_listener(self, protocol=LISTENER_PROTOCOL_HTTPS, address="*"): + resource_uri = self._SERVICE_LISTENER_URI % {'protocol': protocol, + 'address': address} + return self._parse_listener_xml(self._get_resource(resource_uri)) + + def delete_listener(self, protocol=LISTENER_PROTOCOL_HTTPS, address="*"): + resource_uri = self._SERVICE_LISTENER_URI % {'protocol': protocol, + 'address': address} + self._delete_resource(resource_uri) + + def create_listener(self, protocol=LISTENER_PROTOCOL_HTTPS, + cert_thumbprint=None, address="*", enabled=True): + resource_uri = self._SERVICE_LISTENER_URI % {'protocol': protocol, + 'address': address} + self._create_resource( resource_uri, '' - '%(enabled_str)s' + '%(enabled)s' '%(cert_thumbprint)s' '' 'wsman' - '' % {"enabled_str": enabled_str, + '' % {"enabled": self._get_xml_bool(enabled), "cert_thumbprint": cert_thumbprint}) def get_auth_config(self): data = {} - session = self._get_wsman_session() - data_xml = session.Get(self._SERVICE_AUTH_URI) + data_xml = self._get_resource(self._SERVICE_AUTH_URI) tree = ElementTree.fromstring(data_xml) for node in tree: tag = self._get_node_tag(node.tag) @@ -142,13 +208,9 @@ class WinRMConfig(object): for (tag, value) in tag_map.items(): if value is not None: - if value: - new_value = "true" - else: - new_value = "false" - node = tree.find('.//cfg:%s' % tag, namespaces=ns) + new_value = self._get_xml_bool(value) if node.text.lower() != new_value: node.text = new_value data_xml = ElementTree.tostring(tree) diff --git a/cloudbaseinit/plugins/windows/x509.py b/cloudbaseinit/plugins/windows/x509.py index 8f378e9e..99e161d5 100644 --- a/cloudbaseinit/plugins/windows/x509.py +++ b/cloudbaseinit/plugins/windows/x509.py @@ -30,6 +30,10 @@ free = ctypes.cdll.msvcrt.free free.restype = None free.argtypes = [ctypes.c_void_p] +STORE_NAME_MY = "My" +STORE_NAME_ROOT = "Root" +STORE_NAME_TRUSTED_PEOPLE = "TrustedPeople" + class CryptoAPICertManager(object): def _get_cert_thumprint(self, cert_context_p): @@ -101,7 +105,7 @@ class CryptoAPICertManager(object): cryptoapi.CryptReleaseContext(crypt_prov_handle, 0) def create_self_signed_cert(self, subject, validity_years=10, - machine_keyset=True, store_name="MY"): + machine_keyset=True, store_name=STORE_NAME_MY): subject_encoded = None cert_context_p = None store_handle = None @@ -208,7 +212,7 @@ class CryptoAPICertManager(object): return base64_cert_data.replace("\n", "") def import_cert(self, cert_data, machine_keyset=True, - store_name="TrustedPeople"): + store_name=STORE_NAME_MY): base64_cert_data = self._get_cert_base64(cert_data)