diff --git a/samples/vsphere/common/sso.py b/samples/vsphere/common/sso.py
index f0aea9ab..9fbd4d5c 100644
--- a/samples/vsphere/common/sso.py
+++ b/samples/vsphere/common/sso.py
@@ -12,37 +12,38 @@
"""
__author__ = 'VMware, Inc.'
-__copyright__ = 'Copyright 2013, 2016 VMware, Inc. All rights reserved.'
+__copyright__ = 'Copyright 2013, 2016, 2017 VMware, Inc. All rights reserved.'
-# Standard library imports.
-try:
- import httplib
-except ImportError:
- import http.client as httplib
-import base64
-import cgi
-import hashlib
+#Standard library imports.
+import six.moves.http_client
import re
+from six import PY3
+if PY3:
+ from html import escape
+else:
+ from cgi import escape
import sys
import time
+import base64
+import hashlib
+
+from pyVmomi import ThumbprintMismatchException
+
from uuid import uuid4
-
-try:
- from StringIO import StringIO
-except ImportError:
- from io import StringIO
-try:
- from urlparse import urlparse
-except ImportError:
- from urllib.parse import urlparse
-
-# Third-party imports.
+from io import BytesIO
+from six.moves.urllib.parse import urlparse
+#Third-party imports.
from lxml import etree
+from OpenSSL import crypto
+import ssl
+UTF_8 = 'utf-8'
+SHA256 = 'sha256'
+SHA512 = 'sha512'
def _extract_certificate(cert):
- """
+ '''
Extract DER certificate/private key from DER/base64-ed DER/PEM string.
@type cert: C{str}
@@ -50,7 +51,7 @@ def _extract_certificate(cert):
@rtype: C{str}
@return: Certificate/private key in DER (binary ASN.1) format.
- """
+ '''
if not cert:
raise IOError('Empty certificate')
signature = cert[0]
@@ -68,11 +69,11 @@ def _extract_certificate(cert):
class SoapException(Exception):
- """
+ '''
Exception raised in case of STS request failure.
- """
+ '''
def __init__(self, soap_msg, fault_code, fault_string):
- """
+ '''
Initializer for SoapException.
@type soap_msg: C{str}
@@ -81,30 +82,30 @@ class SoapException(Exception):
@param fault_code: The fault code returned by STS.
@type fault_string: C{str}
@param fault_string: The fault string returned by STS.
- """
+ '''
self._soap_msg = soap_msg
self._fault_code = fault_code
self._fault_string = fault_string
Exception.__init__(self)
def __str__(self):
- """
+ '''
Returns the string representation of SoapException.
@rtype: C{str}
@return: string representation of SoapException
- """
+ '''
return ("SoapException:\nfaultcode: %(_fault_code)s\n"
"faultstring: %(_fault_string)s\n"
"faultxml: %(_soap_msg)s" % self.__dict__)
-class SSOHTTPSConnection(httplib.HTTPSConnection):
- """
+class SSOHTTPSConnection(six.moves.http_client.HTTPSConnection):
+ '''
An HTTPS class that verifies server's certificate on connect.
- """
+ '''
def __init__(self, *args, **kwargs):
- """
+ '''
Initializer. See httplib.HTTPSConnection for other arguments
than thumbprint and server_cert.
@@ -118,7 +119,7 @@ class SSOHTTPSConnection(httplib.HTTPSConnection):
@type server_cert: C(str)
@param server_cert: File with expected server certificate.
May be None.
- """
+ '''
self.server_thumbprint = kwargs.pop('thumbprint')
if self.server_thumbprint is not None:
self.server_thumbprint = re.sub(':', '',
@@ -126,14 +127,14 @@ class SSOHTTPSConnection(httplib.HTTPSConnection):
server_cert_path = kwargs.pop('server_cert')
if server_cert_path is not None:
with open(server_cert_path, 'rb') as f:
- server_cert = f.read()
+ server_cert = f.read().decode(UTF_8)
self.server_cert = _extract_certificate(server_cert)
else:
self.server_cert = None
- httplib.HTTPSConnection.__init__(self, *args, **kwargs)
+ six.moves.http_client.HTTPSConnection.__init__(self, *args, **kwargs)
def _check_cert(self, peerCert):
- """
+ '''
Verify that peer certificate matches one we expect.
@type peerCert: C(str)
@@ -141,43 +142,45 @@ class SSOHTTPSConnection(httplib.HTTPSConnection):
@rtype: boolean
@return: True if peerCert is acceptable. False otherwise.
- """
+ '''
if self.server_cert is not None:
if peerCert != self.server_cert:
- return False
+ self.sock.close()
+ self.sock = None
+ raise IOError("Invalid certificate")
if self.server_thumbprint is not None:
thumbprint = hashlib.sha1(peerCert).hexdigest().lower() # pylint: disable=E1101
if thumbprint != self.server_thumbprint:
- return False
- return True
+ self.sock.close()
+ self.sock = None
+ raise ThumbprintMismatchException(
+ expected=self.server_thumbprint, actual=thumbprint)
def connect(self):
- """
+ '''
Connect method: connects to the remote system, and upon
successful connection validates certificate.
Throws an exception when something is wrong. See
httplib.HTTPSConnection.connect() for details.
- """
- httplib.HTTPSConnection.connect(self)
- if not self._check_cert(self.sock.getpeercert(True)):
- self.sock.close()
- self.sock = None
- raise IOError('Invalid certificate')
+ '''
+ six.moves.http_client.HTTPSConnection.connect(self)
+
+ self._check_cert(self.sock.getpeercert(True))
class SsoAuthenticator(object):
- """
+ '''
A class to handle the transport layer communication between the client and
the STS service.
- """
+ '''
def __init__(self,
sts_url,
sts_cert=None,
thumbprint=None
):
- """
+ '''
Initializer for SsoAuthenticator.
@type sts_url: C{str}
@@ -191,7 +194,7 @@ class SsoAuthenticator(object):
@param thumbprint: The SHA-1 thumbprint of the certificate used
by the Security Token Service. It is same
thumbprint you can pass to pyVmomi SoapAdapter.
- """
+ '''
self._sts_cert = sts_cert
self._sts_url = sts_url
self._sts_thumbprint = thumbprint
@@ -201,7 +204,7 @@ class SsoAuthenticator(object):
public_key=None,
private_key=None,
ssl_context=None):
- """
+ '''
Performs a Holder-of-Key SAML token request using the service user's
certificates or a bearer token request using the user credentials.
@@ -213,16 +216,16 @@ class SsoAuthenticator(object):
@type private_key: C{str}
@param private_key: File containing the private key for the service
user registered with SSO, in PEM format.
- @type ssl_context: ssl.SSLContext
- @param ssl_context: Context describing the various SSL options.
+ @type ssl_context: C{ssl.SSLContext}
+ @param ssl_context: SSL context describing the various SSL options.
+ It is only supported in Python 2.7.9 or higher.
@rtype: C{str}
@return: Response received from the STS after the HoK request.
- """
+ '''
parsed = urlparse(self._sts_url)
host = parsed.netloc # pylint: disable=E1101
-
- import ssl
- if ssl_context and hasattr(ssl, '_create_unverified_context'):
+ encoded_message = soap_message.encode(UTF_8)
+ if hasattr(ssl, '_create_unverified_context'):
# Python 2.7.9 has stronger SSL certificate validation, so we need
# to pass in a context when dealing with self-signed certificates.
webservice = SSOHTTPSConnection(host=host,
@@ -232,8 +235,8 @@ class SsoAuthenticator(object):
thumbprint=self._sts_thumbprint,
context=ssl_context)
else:
- # Versions of Python before 2.7.9 don't support the context
- # parameter, so if it wan't provided, don't pass it on.
+ # Versions of Python before 2.7.9 don't support
+ # the context parameter, so don't pass it on.
webservice = SSOHTTPSConnection(host=host,
key_file=private_key,
cert_file=public_key,
@@ -245,22 +248,21 @@ class SsoAuthenticator(object):
webservice.putheader("User-Agent", "VMware/pyVmomi")
webservice.putheader("Accept", "text/xml, multipart/related")
webservice.putheader("Content-type", "text/xml; charset=\"UTF-8\"")
- webservice.putheader("Content-length", "%d" % len(soap_message))
+ webservice.putheader("Content-length", "%d" % len(encoded_message))
webservice.putheader("Connection", "keep-alive")
webservice.putheader("SOAPAction",
- "http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue")
+ "http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue")
webservice.endheaders()
- if sys.version_info[0] >= 3: # Python 3
- webservice.send(bytes(soap_message, 'UTF-8'))
- else:
- webservice.send(soap_message)
+ webservice.send(encoded_message)
saml_response = webservice.getresponse()
if saml_response.status != 200:
- fault = saml_response.read()
+ faultraw = saml_response.read()
+ # Hopefully it is utf-8 or us-ascii, not Apache error message in Shift-JIS.
+ fault = faultraw.decode(UTF_8)
# Best effort at figuring out a SOAP fault.
if saml_response.status == 500 and fault and 'faultcode' in fault:
- fault_xml = etree.fromstring(fault)
+ fault_xml = etree.fromstring(faultraw)
parsed_fault = fault_xml.xpath("//text()")
if len(parsed_fault) == 2:
raise SoapException(fault, *parsed_fault)
@@ -278,7 +280,7 @@ class SsoAuthenticator(object):
delegatable=False,
renewable=False,
ssl_context=None):
- """
+ '''
Extracts the assertion from the Bearer Token received from the Security
Token Service.
@@ -307,11 +309,12 @@ class SsoAuthenticator(object):
@type delegatable: C{boolean}
@param delegatable: Whether the generated token is delegatable or not
The default value is False
- @type ssl_context: ssl.SSLContext
- @param ssl_context: Context describing the various SSL options.
+ @type ssl_context: C{ssl.SSLContext}
+ @param ssl_context: SSL context describing the various SSL options.
+ It is only supported in Python 2.7.9 or higher.
@rtype: C{str}
- @return: The SAML assertion.
- """
+ @return: The SAML assertion in Unicode.
+ '''
request = SecurityTokenRequest(username=username,
password=password,
public_key=public_key,
@@ -324,35 +327,343 @@ class SsoAuthenticator(object):
public_key,
private_key,
ssl_context)
- if sys.version_info[0] >= 3:
- return etree.tostring(
- _extract_element(etree.fromstring(bearer_token),
- 'Assertion',
- {'saml2': "urn:oasis:names:tc:SAML:2.0:assertion"}),
- pretty_print=False).decode('utf-8')
- else:
- return etree.tostring(
- _extract_element(etree.fromstring(bearer_token),
- 'Assertion',
- {'saml2': "urn:oasis:names:tc:SAML:2.0:assertion"}),
- pretty_print=False)
+ return etree.tostring(
+ _extract_element(etree.fromstring(bearer_token),
+ 'Assertion',
+ {'saml2': "urn:oasis:names:tc:SAML:2.0:assertion"}),
+ pretty_print=False).decode(UTF_8)
+ def _get_gss_soap_response(self,
+ binary_token,
+ request_duration=60,
+ token_duration=600,
+ delegatable=False,
+ renewable=False,
+ ssl_context=None):
+ '''
+ Extracts the assertion from the Bearer Token received from the Security
+ Token Service using the binary token generated using either sspi or gss module.
+
+ @type binary_token: C{str}
+ @param binary_token: The security token in base64 encoded format
+
+ @type request_duration: C{long}
+ @param request_duration: The duration for which the request is valid. If
+ the STS receives this request after this
+ duration, it is assumed to have expired. The
+ duration is in seconds and the default is 60s.
+ @type token_duration: C{long}
+ @param token_duration: The duration for which the SAML token is issued
+ for. The duration is specified in seconds and
+ the default is 600s.
+ @type delegatable: C{boolean}
+ @param delegatable: Whether the generated token is delegatable or not
+ The default value is False
+ @type renewable: C{boolean}
+ @param renewable: Whether the generated token is renewable or not
+ The default value is False
+ @type ssl_context: C{ssl.SSLContext}
+ @param ssl_context: SSL context describing the various SSL options.
+ It is only supported in Python 2.7.9 or higher.
+ @rtype: C{str}
+ @return: The SAML assertion.
+ '''
+ request = SecurityTokenRequest(request_duration=request_duration,
+ token_duration=token_duration,
+ gss_binary_token=binary_token)
+ soap_message = request.construct_bearer_token_request_with_binary_token(
+ delegatable=delegatable, renewable=renewable)
+ return self.perform_request(soap_message,
+ ssl_context=ssl_context)
+
+ def _get_bearer_saml_assertion_win(self,
+ request_duration=60,
+ token_duration=600,
+ delegatable=False,
+ renewable=False,
+ ssl_context=None):
+ '''
+ Extracts the assertion from the Bearer Token received from the Security
+ Token Service using the SSPI module.
+
+ @type request_duration: C{long}
+ @param request_duration: The duration for which the request is valid. If
+ the STS receives this request after this
+ duration, it is assumed to have expired. The
+ duration is in seconds and the default is 60s.
+ @type token_duration: C{long}
+ @param token_duration: The duration for which the SAML token is issued
+ for. The duration is specified in seconds and
+ the default is 600s.
+ @type delegatable: C{boolean}
+ @param delegatable: Whether the generated token is delegatable or not
+ The default value is False
+ @type renewable: C{boolean}
+ @param renewable: Whether the generated token is renewable or not
+ The default value is False
+ @type ssl_context: C{ssl.SSLContext}
+ @param ssl_context: SSL context describing the various SSL options.
+ It is only supported in Python 2.7.9 or higher.
+ @rtype: C{str}
+ @return: The SAML assertion.
+ '''
+ import sspi, win32api
+ spn = "sts/%s.com" % win32api.GetDomainName()
+ sspiclient = sspi.ClientAuth("Kerberos", targetspn=spn)
+ in_buf = None
+ err = True
+ # The following will keep running unless we receive a saml token or an error
+ while True:
+ err, out_buf = sspiclient.authorize(in_buf)
+ sectoken = base64.b64encode(out_buf[0].Buffer)
+ soap_response = self._get_gss_soap_response(sectoken,
+ request_duration, token_duration,
+ delegatable, renewable, ssl_context)
+ et = etree.fromstring(soap_response)
+ try:
+ # Check if we have received a challenge token from the server
+ element = _extract_element(et,
+ 'BinaryExchange',
+ {'ns': "http://docs.oasis-open.org/ws-sx/ws-trust/200512"})
+ negotiate_token = element.text
+ out_buf[0].Buffer = base64.b64decode(negotiate_token)
+ in_buf = out_buf
+ except KeyError:
+ # Response does not contain the negotiate token.
+ # It should contain SAML token then.
+ saml_token = etree.tostring(
+ _extract_element(
+ et,
+ 'Assertion',
+ {'saml2': "urn:oasis:names:tc:SAML:2.0:assertion"}),
+ pretty_print=False).decode(UTF_8)
+ break
+ return saml_token
+
+ def _get_bearer_saml_assertion_lin(self,
+ request_duration=60,
+ token_duration=600,
+ delegatable=False,
+ renewable=False):
+ '''
+ Extracts the assertion from the Bearer Token received from the Security
+ Token Service using kerberos.
+
+ @type request_duration: C{long}
+ @param request_duration: The duration for which the request is valid. If
+ the STS receives this request after this
+ duration, it is assumed to have expired. The
+ duration is in seconds and the default is 60s.
+ @type token_duration: C{long}
+ @param token_duration: The duration for which the SAML token is issued
+ for. The duration is specified in seconds and
+ the default is 600s.
+ @type delegatable: C{boolean}
+ @param delegatable: Whether the generated token is delegatable or not
+ The default value is False
+ @type renewable: C{boolean}
+ @param renewable: Whether the generated token is renewable or not
+ The default value is False
+ @rtype: C{str}
+ @return: The SAML assertion in Unicode.
+ '''
+ import kerberos, platform
+ service = 'host@%s' % platform.node()
+ _, context = kerberos.authGSSClientInit(service, 0)
+ challenge = ''
+ # The following will keep running unless we receive a saml token or an error
+ while True:
+ # Call GSS step
+ result = kerberos.authGSSClientStep(context, challenge)
+ if result < 0:
+ break
+ sectoken = kerberos.authGSSClientResponse(context)
+ soap_response = self._get_gss_soap_response(sectoken,
+ request_duration, token_duration, delegatable,
+ renewable)
+ et = etree.fromstring(soap_response)
+ try:
+ # Check if we have received a challenge token from the server
+ element = _extract_element(et,
+ 'BinaryExchange',
+ {'ns': "http://docs.oasis-open.org/ws-sx/ws-trust/200512"})
+ negotiate_token = element.text
+ challenge = negotiate_token
+ except KeyError:
+ # Response does not contain the negotiate token.
+ # It should contain SAML token then.
+ saml_token = etree.tostring(
+ _extract_element(
+ et,
+ 'Assertion',
+ {'saml2': "urn:oasis:names:tc:SAML:2.0:assertion"}),
+ pretty_print=False).decode(UTF_8)
+ break
+ return saml_token
+
+ def get_bearer_saml_assertion_gss_api(self,
+ request_duration=60,
+ token_duration=600,
+ delegatable=False,
+ renewable=False):
+ '''
+ Extracts the assertion from the Bearer Token received from the Security
+ Token Service using the GSS API.
+
+ @type request_duration: C{long}
+ @param request_duration: The duration for which the request is valid. If
+ the STS receives this request after this
+ duration, it is assumed to have expired. The
+ duration is in seconds and the default is 60s.
+ @type token_duration: C{long}
+ @param token_duration: The duration for which the SAML token is issued
+ for. The duration is specified in seconds and
+ the default is 600s.
+ @type delegatable: C{boolean}
+ @param delegatable: Whether the generated token is delegatable or not
+ The default value is False
+ @type renewable: C{boolean}
+ @param renewable: Whether the generated token is renewable or not
+ The default value is False
+ @rtype: C{str}
+ @return: The SAML assertion.
+ '''
+ if sys.platform == "win32":
+ saml_token = self._get_bearer_saml_assertion_win(request_duration,
+ token_duration, delegatable, renewable)
+ else:
+ raise Exception("Currently, not supported on this platform")
+ ## TODO Remove this exception once SSO supports validation of tickets
+ # generated against host machines
+ # saml_token = self._get_bearer_saml_assertion_lin(request_duration, token_duration, delegatable)
+ return saml_token
+
+ def get_hok_saml_assertion(self,
+ public_key,
+ private_key,
+ request_duration=60,
+ token_duration=600,
+ act_as_token=None,
+ delegatable=False,
+ renewable=False,
+ ssl_context=None):
+ '''
+ Extracts the assertion from the response received from the Security
+ Token Service.
+
+ @type public_key: C{str}
+ @param public_key: File containing the public key for the service
+ user registered with SSO, in PEM format.
+ @type private_key: C{str}
+ @param private_key: File containing the private key for the service
+ user registered with SSO, in PEM format.
+ @type request_duration: C{long}
+ @param request_duration: The duration for which the request is valid. If
+ the STS receives this request after this
+ duration, it is assumed to have expired. The
+ duration is in seconds and the default is 60s.
+ @type token_duration: C{long}
+ @param token_duration: The duration for which the SAML token is issued
+ for. The duration is specified in seconds and
+ the default is 600s.
+ @type act_as_token: C{str}
+ @param act_as_token: Bearer/Hok token which is delegatable
+ @type delegatable: C{boolean}
+ @param delegatable: Whether the generated token is delegatable or not
+ @type renewable: C{boolean}
+ @param renewable: Whether the generated token is renewable or not
+ The default value is False
+ @type ssl_context: C{ssl.SSLContext}
+ @param ssl_context: SSL context describing the various SSL options.
+ It is only supported in Python 2.7.9 or higher.
+ @rtype: C{str}
+ @return: The SAML assertion in Unicode.
+ '''
+ request = SecurityTokenRequest(public_key=public_key,
+ private_key=private_key,
+ request_duration=request_duration,
+ token_duration=token_duration)
+ soap_message = request.construct_hok_request(delegatable=delegatable,
+ act_as_token=act_as_token,
+ renewable=renewable)
+ hok_token = self.perform_request(soap_message,
+ public_key,
+ private_key,
+ ssl_context)
+ return etree.tostring(
+ _extract_element(
+ etree.fromstring(hok_token),
+ 'Assertion',
+ {'saml2': "urn:oasis:names:tc:SAML:2.0:assertion"}),
+ pretty_print=False).decode(UTF_8)
+
+ def get_token_by_token(self,
+ hok_token,
+ private_key,
+ request_duration=60,
+ token_duration=600,
+ renewable=False,
+ ssl_context=None):
+ """
+ Get Hok token by Hok token.
+
+ @type hok_token: C{str}
+ @param hok_token: Hok token to be used to get another token
+ @type private_key: C{str}
+ @param private_key: File containing the private key for the service
+ user registered with SSO, in PEM format.
+ @type request_duration: C{long}
+ @param request_duration: The duration for which the request is valid. If
+ the STS receives this request after this
+ duration, it is assumed to have expired. The
+ duration is in seconds and the default is 60s.
+ @type token_duration: C{long}
+ @param token_duration: The duration for which the SAML token is issued
+ for. The duration is specified in seconds and
+ the default is 600s.
+ @type renewable: C{boolean}
+ @param renewable: Whether the generated token is renewable or not
+ The default value is False
+ @type ssl_context: C{ssl.SSLContext}
+ @param ssl_context: SSL context describing the various SSL options.
+ It is only supported in Python 2.7.9 or higher.
+ @rtype: C{str}
+ @return: The Hok SAML assertion in Unicode.
+ """
+ request = SecurityTokenRequest(private_key=private_key,
+ request_duration=request_duration,
+ token_duration=token_duration,
+ hok_token=hok_token)
+ soap_message = request.construct_hok_by_hok_request(renewable=renewable)
+
+ soap_message = add_saml_context(soap_message, hok_token, private_key)
+
+ hok_token = self.perform_request(soap_message=soap_message,
+ ssl_context=ssl_context)
+ return etree.tostring(
+ _extract_element(
+ etree.fromstring(hok_token),
+ 'Assertion',
+ {'saml2': "urn:oasis:names:tc:SAML:2.0:assertion"}),
+ pretty_print=False).decode(UTF_8)
class SecurityTokenRequest(object):
- """
+ '''
SecurityTokenRequest class handles the serialization of request to the STS
for a SAML token.
- """
+ '''
- # pylint: disable=R0902
+ #pylint: disable=R0902
def __init__(self,
username=None,
password=None,
public_key=None,
private_key=None,
request_duration=60,
- token_duration=600):
- """
+ token_duration=600,
+ gss_binary_token=None,
+ hok_token=None):
+ '''
Initializer for the SecurityToken Request class.
@type username: C{str}
@@ -377,7 +688,7 @@ class SecurityTokenRequest(object):
@param token_duraiton: The duration for which the SAML token is issued
for. The duration is specified in seconds and
the default is 600s.
- """
+ '''
self._timestamp_id = _generate_id()
self._signature_id = _generate_id()
self._request_id = _generate_id()
@@ -391,20 +702,45 @@ class SecurityTokenRequest(object):
time.gmtime(current +
request_duration))
self._timestamp = TIMESTAMP_TEMPLATE % self.__dict__
- self._username = cgi.escape(username) if username else username
- self._password = cgi.escape(password) if password else password
+ self._username = escape(username) if username else username
+ self._password = escape(password) if password else password
self._public_key_file = public_key
self._private_key_file = private_key
self._act_as_token = None
self._renewable = str(False).lower()
self._delegatable = str(False).lower()
- self._use_key = ''
+ self._use_key = ""
self._private_key = None
self._binary_exchange = None
self._public_key = None
+ if gss_binary_token:
+ self._binary_exchange = BINARY_EXCHANGE_TEMPLATE % gss_binary_token
+ #The following are populated later. Set to None here to keep in-line
+ #with PEP8.
+ self._binary_security_token = None
+ self._hok_token = hok_token
+ self._key_type = None
+ self._security_token = None
+ self._signature_text = None
+ self._signature = None
+ self._signed_info = None
+ self._timestamp_digest = None
+ self._signature_value = None
+ self._xml_text = None
+ self._xml = None
+ self._request_digest = None
+
+ #These will only be populated if requesting an HoK token.
+ if self._private_key_file:
+ with open(self._private_key_file) as fp:
+ self._private_key = fp.read()
+
+ if self._public_key_file:
+ with open(self._public_key_file) as fp:
+ self._public_key = fp.read()
def construct_bearer_token_request(self, delegatable=False, renewable=False):
- """
+ '''
Constructs the actual Bearer token SOAP request.
@type delegatable: C{boolean}
@@ -414,26 +750,230 @@ class SecurityTokenRequest(object):
The default value is False
@rtype: C{str}
@return: Bearer token SOAP request.
- """
+ '''
self._key_type = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer"
self._security_token = USERNAME_TOKEN_TEMPLATE % self.__dict__
self._delegatable = str(delegatable).lower()
self._renewable = str(renewable).lower()
return _canonicalize(REQUEST_TEMPLATE % self.__dict__)
+ def construct_bearer_token_request_with_binary_token(self,
+ delegatable=False,
+ renewable=False):
+ '''
+ Constructs the actual Bearer token SOAP request using the binary exchange GSS/SSPI token.
+
+ @type delegatable: C{boolean}
+ @param delegatable: Whether the generated token is delegatable or not
+ @type renewable: C{boolean}
+ @param renewable: Whether the generated token is renewable or not
+ The default value is False
+ @rtype: C{str}
+ @return: Bearer token SOAP request.
+ '''
+ self._key_type = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer"
+ self._delegatable = str(delegatable).lower()
+ self._renewable = str(renewable).lower()
+ return _canonicalize(GSS_REQUEST_TEMPLATE % self.__dict__)
+
+ def construct_hok_request(self, delegatable=False, act_as_token=None,
+ renewable=False):
+ '''
+ Constructs the actual HoK token SOAP request.
+
+ @type delegatable: C{boolean}
+ @param delegatable: Whether the generated token is delegatable or not
+ @type act_as_token: C{str}
+ @param act_as_token: Bearer/Hok token which is delegatable
+ @type renewable: C{boolean}
+ @param renewable: Whether the generated token is renewable or not
+ The default value is False
+ @rtype: C{str}
+ @return: HoK token SOAP request in Unicode.
+ '''
+ self._binary_security_token = base64.b64encode(
+ _extract_certificate(self._public_key)).decode(UTF_8)
+ self._use_key = USE_KEY_TEMPLATE % self.__dict__
+ self._security_token = BINARY_SECURITY_TOKEN_TEMPLATE % self.__dict__
+ self._key_type = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/PublicKey"
+ self._renewable = str(renewable).lower()
+ self._delegatable = str(delegatable).lower()
+ self._act_as_token = act_as_token
+ if act_as_token is None:
+ self._xml_text = _canonicalize(REQUEST_TEMPLATE % self.__dict__)
+ else:
+ self._xml_text = ACTAS_REQUEST_TEMPLATE % self.__dict__
+ self.sign_request()
+ return etree.tostring(self._xml, pretty_print=False).decode(UTF_8)
+
+ def construct_hok_by_hok_request(self, renewable=False):
+ """
+ @type renewable: C{boolean}
+ @param renewable: Whether the generated token is renewable or not
+ The default value is False
+ @rtype: C{str}
+ @return: HoK token SOAP request in Unicode.
+ """
+ self._renewable = str(renewable).lower()
+ self._key_type = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/PublicKey"
+ return _canonicalize(REQUEST_TEMPLATE_TOKEN_BY_TOKEN) % self.__dict__
+
+ def sign_request(self):
+ '''
+ Calculates the signature to the header of the SOAP request which can be
+ used by the STS to verify that the SOAP message originated from a
+ trusted service.
+ '''
+ base_xml = etree.fromstring(self._xml_text)
+ request_tree = _extract_element(base_xml,
+ 'Body',
+ {'SOAP-ENV': "http://schemas.xmlsoap.org/soap/envelope/"})
+ request = _canonicalize(etree.tostring(request_tree))
+ request_tree = _extract_element(base_xml,
+ 'Timestamp',
+ {'ns3': "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"})
+ timestamp = _canonicalize(etree.tostring(request_tree))
+ self._request_digest = _make_hash(request.encode(UTF_8)).decode(UTF_8) # pylint: disable=W0612
+ self._timestamp_digest = _make_hash(timestamp.encode(UTF_8)).decode(UTF_8) # pylint: disable=W0612
+ self._algorithm = SHA256
+ self._signed_info = _canonicalize(SIGNED_INFO_TEMPLATE % self.__dict__)
+ self._signature_value = _sign(self._private_key, self._signed_info).decode(UTF_8)
+ self._signature_text = _canonicalize(SIGNATURE_TEMPLATE % self.__dict__)
+ self.embed_signature()
+
+ def embed_signature(self):
+ '''
+ Embeds the signature in to the header of the SOAP request.
+ '''
+ self._xml = etree.fromstring(self._xml_text)
+ security = _extract_element(self._xml,
+ 'Security',
+ {'ns6': "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"})
+ self._signature = etree.fromstring(self._signature_text)
+ security.append(self._signature)
+ self._xml_text = etree.tostring(self._xml).decode(UTF_8)
+
+
+def add_saml_context(serialized_request, saml_token, private_key_file):
+ '''
+ A helper method provided to sign the outgoing LoginByToken requests with the
+ HoK token.
+
+ @type serialized_request: C{str}
+ @param serialized_request: SOAP request which needs to be signed.
+ @type saml_token: C{str}
+ @param saml_token: SAML assertion that will be added to the SOAP
+ request.
+ @type private_key_file: C{str}
+ @param private_key_file: Private key of the service user that will be
+ used to sign the request, in PEM format.
+ @rtype: C{str}
+ @return: signed SOAP request in Unicode.
+ '''
+ with open(private_key_file) as fp:
+ private_key = fp.read()
+ xml = etree.fromstring(serialized_request)
+ value_map = {}
+ value_map['_request_id'] = _generate_id()
+ request_body = _extract_element(xml,
+ 'Body',
+ {'soapenv': "http://schemas.xmlsoap.org/soap/envelope/"})
+ request_body.nsmap["wsu"] = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
+ request_body.set("{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd}Id", value_map['_request_id'])
+ value_map['_request_digest'] = _make_hash_sha512(
+ _canonicalize(etree.tostring(request_body))
+ .encode(UTF_8)).decode(UTF_8)
+ security = _extract_element(xml,
+ 'Security',
+ {'ns6': "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"})
+ current = time.time()
+ value_map['_created'] = time.strftime(TIME_FORMAT,
+ time.gmtime(current))
+ value_map['_request_expires'] = time.strftime(TIME_FORMAT,
+ time.gmtime(current + 600))
+ value_map['_timestamp_id'] = _generate_id()
+ timestamp = _canonicalize(TIMESTAMP_TEMPLATE % value_map)
+ value_map['_timestamp_digest'] = _make_hash_sha512(
+ timestamp.encode(UTF_8)).decode(UTF_8)
+
+ security.append(etree.fromstring(timestamp))
+ value_map['_algorithm'] = SHA512
+ value_map['_signed_info'] = _canonicalize(SIGNED_INFO_TEMPLATE % value_map)
+ value_map['_signature_value'] = _sign(private_key,
+ value_map['_signed_info'],
+ SHA512).decode(UTF_8)
+ value_map['samlId'] = etree.fromstring(saml_token).get("ID")
+ signature = etree.fromstring(_canonicalize(REQUEST_SIGNATURE_TEMPLATE %
+ value_map))
+ security.append(signature)
+ return etree.tostring(xml, pretty_print=False).decode(UTF_8)
+
def _generate_id():
- """
+ '''
An internal helper method to generate UUIDs.
@rtype: C{str}
@return: UUID
- """
+ '''
return "_%s" % uuid4()
+def _load_private_key(der_key):
+ '''
+ An internal helper to load private key.
+
+ @type der_key: C{str}
+ @param der_key: The private key, in DER format.
+
+ @rtype: crypto.privatekey
+ @return: Loaded private key.
+ '''
+
+ # OpenSSL 0.9.8 does not handle correctly PKCS8 keys passed in DER format
+ # (only PKCS1 keys are understood in DER format).
+
+ # Unencrypted PKCS8, or PKCS1 for OpenSSL 1.0.1, PKCS1 for OpenSSL 0.9.8
+ try:
+ return crypto.load_privatekey(crypto.FILETYPE_ASN1, der_key, '')
+ except (crypto.Error, ValueError):
+ pass
+ # Unencrypted PKCS8 for OpenSSL 0.9.8, and PKCS1, just in case...
+ for key_type in ('PRIVATE KEY', 'RSA PRIVATE KEY'):
+ try:
+ return crypto.load_privatekey(crypto.FILETYPE_PEM,
+ '-----BEGIN ' + key_type + '-----\n' +
+ base64.encodestring(der_key).decode(UTF_8) +
+ '-----END ' + key_type + '-----\n',
+ b'')
+ except (crypto.Error, ValueError):
+ pass
+ # We could try 'ENCRYPTED PRIVATE KEY' here - but we do not know passphrase.
+ raise
+
+def _sign(private_key, data, digest=SHA256):
+ '''
+ An internal helper method to sign the 'data' with the 'private_key'.
+
+ @type private_key: C{str}
+ @param private_key: The private key used to sign the 'data', in one of
+ supported formats.
+ @type data: C{str}
+ @param data: The data that needs to be signed.
+ @type digest: C{str}
+ @param digest: Digest is a str naming a supported message digest type,
+ for example 'sha256'.
+
+ @rtype: C{str}
+ @return: Signed string.
+ '''
+ # Convert private key in arbitrary format into DER (DER is binary format
+ # so we get rid of \n / \r\n differences, and line breaks in PEM).
+ pkey = _load_private_key(_extract_certificate(private_key))
+ return base64.b64encode(crypto.sign(pkey, data, digest))
+
def _canonicalize(xml_string):
- """
+ '''
Given an xml string, canonicalize the string per
U{http://www.w3.org/2001/10/xml-exc-c14n#}
@@ -441,25 +981,16 @@ def _canonicalize(xml_string):
@param xml_string: The XML string that needs to be canonicalized.
@rtype: C{str}
- @return: Canonicalized string.
- """
- string = StringIO(xml_string)
+ @return: Canonicalized string in Unicode.
+ '''
parser = etree.XMLParser(remove_blank_text=True)
- tree = etree.parse(string, parser=parser)
- # io.StringIO only accepts Unicode input (i.e. u"multibyte string"), while StringIO.StringIO accepts either 8 bit input or unicode input.
- if sys.version_info[0] >= 3:
- from io import BytesIO
- string = BytesIO()
- tree.write_c14n(string, exclusive=True, with_comments=False)
- return string.getvalue().decode('utf-8')
- else:
- string = StringIO()
- tree.write_c14n(string, exclusive=True, with_comments=False)
- return string.getvalue()
-
+ tree = etree.fromstring(xml_string, parser=parser).getroottree()
+ string = BytesIO()
+ tree.write_c14n(string, exclusive=True, with_comments=False)
+ return string.getvalue().decode(UTF_8)
def _extract_element(xml, element_name, namespace):
- """
+ '''
An internal method provided to extract an element from the given XML.
@type xml: C{str}
@@ -472,41 +1003,102 @@ def _extract_element(xml, element_name, namespace):
@rtype: etree element.
@return: The extracted element.
- """
+ '''
assert(len(namespace) == 1)
- result = xml.xpath("//%s:%s" % (list(namespace.keys())[0], element_name), # python 3.x dict.keys() returns a view
- namespaces=namespace)
+ result = xml.xpath("//%s:%s" % (list(namespace.keys())[0], element_name),
+ namespaces=namespace)
if result:
return result[0]
else:
- raise KeyError('%s does not seem to be present in the XML.' %
+ raise KeyError("%s does not seem to be present in the XML." %
element_name)
+def _make_hash(data):
+ '''
+ An internal method to calculate the sha256 hash of the data.
+
+ @type data: C{str}
+ @param data: The data for which the hash needs to be calculated.
+
+ @rtype: C{str}
+ @return: Base64 encoded sha256 hash.
+ '''
+ return base64.b64encode(hashlib.sha256(data).digest()) # pylint: disable=E1101
+
+
+def _make_hash_sha512(data):
+ '''
+ An internal method to calculate the sha512 hash of the data.
+
+ @type data: C{str}
+ @param data: The data for which the hash needs to be calculated.
+
+ @rtype: C{str}
+ @return: Base64 encoded sha512 hash.
+ '''
+ return base64.b64encode(hashlib.sha512(data).digest()) # pylint: disable=E1101
+
+
TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.987Z"
+#The SAML token requests usually contain an xmldsig which guarantees that the
+#message hasn't been tampered with during the transport. The following
+#SIGNED_INFO_TEMPLATE is used to construct the signedinfo part of the signature.
+SIGNED_INFO_TEMPLATE = """\
+
+
+
+
+
+
+
+
+%(_request_digest)s
+
+
+
+
+
+
+%(_timestamp_digest)s
+
+
+"""
-# Template container for user's credentials when requesting a bearer token.
-USERNAME_TOKEN_TEMPLATE = """\
-
-%(_username)s
-%(_password)s
-"""
+#The following template is used as the container for signed info in WS-Trust
+#SOAP requests signed with the SAML token. It contains the digest of the
+#signed info, signed with the private key of the Solution user and contains a
+#reference to the actual SAML token which contains the solution user's public
+#key.
+REQUEST_SIGNATURE_TEMPLATE = """\
+
+%(_signed_info)s
+%(_signature_value)s
+
+
+%(samlId)s
+
+
+"""
+#The following template is used as a signed info container for the actual SAML
+#token requests requesting a SAML token. It contains the digest of the signed
+#info signed with the Service User's private key.
+SIGNATURE_TEMPLATE = """\
+
+%(_signed_info)s
+%(_signature_value)s
+
+
+
+
+
+"""
-# Template containing the anchor to the signature.
-USE_KEY_TEMPLATE = """\
-"""
-
-
-# The follwoing template is used to create a timestamp for the various messages.
-# The timestamp is used to indicate the duration of the request itself.
-TIMESTAMP_TEMPLATE = """\
-
-%(_created)s%(_request_expires)s"""
-
-
-# The following template is used to construct the token requests to the STS.
+#The following template is used to construct the token requests to the STS.
REQUEST_TEMPLATE = """\
@@ -535,3 +1127,92 @@ REQUEST_TEMPLATE = """\
http://www.w3.org/2001/04/xmldsig-more#rsa-sha256%(_use_key)s
"""
+
+#The following template is used to construct the token-by-token requests to the STS.
+REQUEST_TEMPLATE_TOKEN_BY_TOKEN = """\
+
+
+
+%(_hok_token)s
+
+
+
+
+urn:oasis:names:tc:SAML:2.0:assertion
+http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue
+
+%(_created)s
+%(_expires)s
+
+
+%(_delegatable)s
+%(_key_type)s
+http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
+
+
+"""
+
+GSS_REQUEST_TEMPLATE = """\
+
+
+
+%(_timestamp)s
+
+
+
+
+urn:oasis:names:tc:SAML:2.0:assertion
+http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue
+
+%(_created)s
+%(_expires)s
+
+
+%(_delegatable)s
+%(_key_type)s
+http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
+%(_binary_exchange)s
+%(_use_key)s
+
+"""
+
+#Template container for the service user's public key when requesting an HoK
+#token.
+BINARY_SECURITY_TOKEN_TEMPLATE = """\
+%(_binary_security_token)s
+"""
+
+#Template container for user's credentials when requesting a bearer token.
+USERNAME_TOKEN_TEMPLATE = """\
+
+%(_username)s
+%(_password)s
+"""
+
+#Template containing the anchor to the signature.
+USE_KEY_TEMPLATE = """\
+"""
+
+#The follwoing template is used to create a timestamp for the various messages.
+#The timestamp is used to indicate the duration of the request itself.
+TIMESTAMP_TEMPLATE = """\
+
+%(_created)s%(_request_expires)s"""
+
+BINARY_EXCHANGE_TEMPLATE = """\
+%s"""
+
+ACTAS_REQUEST_TEMPLATE = """%(_created)s%(_request_expires)s%(_binary_security_token)surn:oasis:names:tc:SAML:2.0:assertionhttp://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue%(_created)s%(_expires)s%(_delegatable)s%(_act_as_token)shttp://docs.oasis-open.org/ws-sx/ws-trust/200512/PublicKeyhttp://www.w3.org/2001/04/xmldsig-more#rsa-sha256"""