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)