class RbVmomi::SSO

Provides access to vCenter Single Sign-On

Constants

BST_PROFILE
C14N_CLASS
C14N_METHOD
DIGEST_METHOD
ENCODING_METHOD
NAMESPACES
SIGNATURE_METHOD
STS_PATH
TOKEN_PROFILE
TOKEN_TYPE

Attributes

assertion[R]
assertion_id[R]
certificate[R]
host[R]
password[R]
path[R]
port[R]
private_key[R]
user[R]

Public Class Methods

new(opts = {}) click to toggle source

Creates an instance of an SSO object

@param [Hash] opts the options to create the object with @option opts [String] :host the host to connect to @option opts [Fixnum] :port (443) the port to connect to @option opts [String] :path the path to call @option opts [String] :user the user to authenticate with @option opts [String] :password the password to authenticate with @option opts [String] :private_key the private key to use @option opts [String] :certificate the certificate to use @option opts [Boolean] :insecure (false) whether to connect insecurely

# File lib/rbvmomi/sso.rb, line 50
def initialize(opts = {})
  @host     = opts[:host]
  @insecure = opts.fetch(:insecure, false)
  @password = opts[:password]
  @path     = opts.fetch(:path, STS_PATH)
  @port     = opts.fetch(:port, 443)
  @user     = opts[:user]

  load_x509(opts[:private_key], opts[:certificate])
end

Public Instance Methods

request_token() click to toggle source
# File lib/rbvmomi/sso.rb, line 61
def request_token
  req = sso_call(hok_token_request)

  unless req.is_a?(Net::HTTPSuccess)
    resp = Nokogiri::XML(req.body)
    resp.remove_namespaces!
    raise(resp.at_xpath('//Envelope/Body/Fault/faultstring/text()'))
  end

  extract_assertion(req.body)
end
sign_request(request) click to toggle source
# File lib/rbvmomi/sso.rb, line 73
def sign_request(request)
  raise('Need SAML2 assertion') unless @assertion
  raise('No SAML2 assertion ID') unless @assertion_id

  request_id = generate_id
  timestamp_id = generate_id

  request = request.is_a?(String) ? Nokogiri::XML(request) : request
  builder = Nokogiri::XML::Builder.new do |xml|
    xml[:soap].Header(Hash[NAMESPACES.map { |ns, uri| ["xmlns:#{ns}", uri] }]) do
      xml[:wsse].Security do
        wsu_timestamp(xml, timestamp_id)
        ds_signature(xml, request_id, timestamp_id) do |x|
          x[:wsse].SecurityTokenReference('wsse11:TokenType' => TOKEN_PROFILE) do
            x[:wsse].KeyIdentifier(
              @assertion_id,
              'ValueType' => 'http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLID'
            )
          end
        end
      end
    end
  end

  # To avoid Nokogiri mangling the token, we replace it as a string
  # later on. Figure out a way around this.
  builder.doc.at_xpath('//soap:Header/wsse:Security/wsu:Timestamp').add_previous_sibling(Nokogiri::XML::Text.new('SAML_ASSERTION_PLACEHOLDER', builder.doc))

  request.at_xpath('//soap:Envelope', NAMESPACES).tap do |e|
    NAMESPACES.each do |ns, uri|
      e.add_namespace(ns.to_s, uri)
    end
  end
  request.xpath('//soap:Envelope/soap:Body').each do |body|
    body.add_previous_sibling(builder.doc.root)
    body.add_namespace('wsu', NAMESPACES[:wsu])
    body['wsu:Id'] = request_id
  end

  signed = sign(request)
  signed.gsub!('SAML_ASSERTION_PLACEHOLDER', @assertion.to_xml(:indent => 0, :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML).strip)

  signed
end
sso_call(body) click to toggle source

We default to Issue, since that's all we currently need.

# File lib/rbvmomi/sso.rb, line 119
def sso_call(body)
  sso_url = URI::HTTPS.build(:host => @host, :port => @port, :path => @path)
  http = Net::HTTP.new(sso_url.host, sso_url.port)
  http.use_ssl = true
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @insecure

  req = Net::HTTP::Post.new(sso_url.request_uri)
  req.add_field('Accept', 'text/xml, multipart/related')
  req.add_field('User-Agent', "VMware/RbVmomi #{RbVmomi::VERSION}")
  req.add_field('SOAPAction', 'http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue')
  req.content_type = 'text/xml; charset="UTF-8"'
  req.body = body

  http.request(req)
end

Private Instance Methods

ds_signature(xml, request_id, timestamp_id, id = nil) { |xml| ... } click to toggle source
# File lib/rbvmomi/sso.rb, line 249
def ds_signature(xml, request_id, timestamp_id, id = nil)
  signature_id = {}
  signature_id['Id'] = id if id
  xml[:ds].Signature(signature_id) do
    ds_signed_info(xml, request_id, timestamp_id)
    xml[:ds].SignatureValue
    xml[:ds].KeyInfo do
      yield xml
    end
  end
end
ds_signed_info(xml, request_id, timestamp_id) click to toggle source
# File lib/rbvmomi/sso.rb, line 261
def ds_signed_info(xml, request_id, timestamp_id)
  xml[:ds].SignedInfo do
    xml[:ds].CanonicalizationMethod('Algorithm' => C14N_METHOD)
    xml[:ds].SignatureMethod('Algorithm' => SIGNATURE_METHOD)
    xml[:ds].Reference('URI' => "##{request_id}") do
      xml[:ds].Transforms do
        xml[:ds].Transform('Algorithm' => C14N_METHOD)
      end
      xml[:ds].DigestMethod('Algorithm' => DIGEST_METHOD)
      xml[:ds].DigestValue
    end
    xml[:ds].Reference('URI' => "##{timestamp_id}") do
      xml[:ds].Transforms do
        xml[:ds].Transform('Algorithm' => C14N_METHOD)
      end
      xml[:ds].DigestMethod('Algorithm' => DIGEST_METHOD)
      xml[:ds].DigestValue
    end
  end
end
extract_assertion(sso_response) click to toggle source
# File lib/rbvmomi/sso.rb, line 185
def extract_assertion(sso_response)
  sso_response = Nokogiri::XML(sso_response) if sso_response.is_a?(String)
  namespaces = sso_response.collect_namespaces

  # Doesn't matter that usually there's more than one NS with the same
  # URI - either will work for XPath. We just don't want to hardcode
  # xmlns:saml2.
  token_ns = namespaces.find { |_, uri| uri == TOKEN_TYPE }.first.gsub(/^xmlns:/, '')

  @assertion = sso_response.at_xpath("//#{token_ns}:Assertion", namespaces)
  @assertion_id = @assertion.at_xpath("//#{token_ns}:Assertion/@ID", namespaces).value
end
generate_id() click to toggle source
# File lib/rbvmomi/sso.rb, line 309
def generate_id
  "_#{SecureRandom.uuid}"
end
hok_token_request() click to toggle source
# File lib/rbvmomi/sso.rb, line 137
def hok_token_request
  request_id = generate_id
  security_token_id = generate_id
  signature_id = generate_id
  timestamp_id = generate_id

  datum = Time.now.utc
  created_at = datum.iso8601
  token_expires_at = (datum + 1800).iso8601

  builder = Nokogiri::XML::Builder.new do |xml|
    xml[:soap].Envelope(Hash[NAMESPACES.map { |ns, uri| ["xmlns:#{ns}", uri] }]) do
      xml[:soap].Header do
        xml[:wsse].Security do
          wsu_timestamp(xml, timestamp_id, datum)
          wsse_username_token(xml)
          wsse_binary_security_token(xml, security_token_id)
          ds_signature(xml, request_id, timestamp_id, signature_id) do |x|
            x[:wsse].SecurityTokenReference do
              x[:wsse].Reference(
                'URI' => "##{security_token_id}",
                'ValueType' => BST_PROFILE
              )
            end
          end
        end
      end
      xml[:soap].Body('wsu:Id' => request_id) do
        xml[:wst].RequestSecurityToken do
          xml[:wst].TokenType(TOKEN_TYPE)
          xml[:wst].RequestType('http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue')
          xml[:wst].Lifetime do
            xml[:wsu].Created(created_at)
            xml[:wsu].Expires(token_expires_at)
          end
          xml[:wst].Renewing('Allow' => 'false', 'OK' => 'false')
          xml[:wst].KeyType('http://docs.oasis-open.org/ws-sx/ws-trust/200512/PublicKey')
          xml[:wst].SignatureAlgorithm(SIGNATURE_METHOD)
          xml[:wst].Delegatable('false')
        end
        xml[:wst].UseKey('Sig' => signature_id)
      end
    end
  end

  sign(builder.doc)
end
load_x509(private_key, certificate) click to toggle source
# File lib/rbvmomi/sso.rb, line 215
def load_x509(private_key, certificate)
  @private_key = private_key ? private_key : OpenSSL::PKey::RSA.new(2048)
  if @private_key.is_a? String
    @private_key = OpenSSL::PKey::RSA.new(@private_key)
  end

  @certificate = certificate
  if @certificate && !private_key
    raise(ArgumentError, "Can't generate private key from a certificate")
  end

  if @certificate.is_a? String
    @certificate = OpenSSL::X509::Certificate.new(@certificate)
  end
  # If only a private key is specified, we will generate a certificate.
  unless @certificate
    timestamp = Time.now.utc
    @certificate = OpenSSL::X509::Certificate.new
    @certificate.not_before = timestamp
    @certificate.not_after = timestamp + 3600 # 3600 is 1 hour
    @certificate.subject = OpenSSL::X509::Name.new([
                                                     %w[O VMware],
                                                     %w[OU RbVmomi],
                                                     %W[CN #{@user}]
                                                   ])
    @certificate.issuer = @certificate.subject
    @certificate.serial = rand(2**160)
    @certificate.public_key = @private_key.public_key
    @certificate.sign(@private_key, OpenSSL::Digest::SHA512.new)
  end

  true
end
sign(doc) click to toggle source
# File lib/rbvmomi/sso.rb, line 198
def sign(doc)
  signature_digest_references = doc.xpath('/soap:Envelope/soap:Header/wsse:Security/ds:Signature/ds:SignedInfo/ds:Reference/@URI', doc.collect_namespaces).map { |a| a.value.sub(/^#/, '') }
  signature_digest_references.each do |ref|
    data = doc.at_xpath("//*[@wsu:Id='#{ref}']", doc.collect_namespaces)
    digest = Base64.strict_encode64(Digest::SHA2.new(512).digest(data.canonicalize(C14N_CLASS)))
    digest_tag = doc.at_xpath("/soap:Envelope/soap:Header/wsse:Security/ds:Signature/ds:SignedInfo/ds:Reference[@URI='##{ref}']/ds:DigestValue", doc.collect_namespaces)
    digest_tag.add_child(Nokogiri::XML::Text.new(digest, doc))
  end

  signed_info = doc.at_xpath('/soap:Envelope/soap:Header/wsse:Security/ds:Signature/ds:SignedInfo', doc.collect_namespaces)
  signature = Base64.strict_encode64(@private_key.sign(OpenSSL::Digest::SHA512.new, signed_info.canonicalize(C14N_CLASS)))
  signature_value_tag = doc.at_xpath('/soap:Envelope/soap:Header/wsse:Security/ds:Signature/ds:SignatureValue', doc.collect_namespaces)
  signature_value_tag.add_child(Nokogiri::XML::Text.new(signature, doc))

  doc.to_xml(:indent => 0, :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML).strip
end
wsse_binary_security_token(xml, id) click to toggle source
# File lib/rbvmomi/sso.rb, line 300
def wsse_binary_security_token(xml, id)
  xml[:wsse].BinarySecurityToken(
    Base64.strict_encode64(@certificate.to_der),
    'EncodingType' => ENCODING_METHOD,
    'ValueType' => BST_PROFILE,
    'wsu:Id' => id
  )
end
wsse_username_token(xml) click to toggle source
# File lib/rbvmomi/sso.rb, line 293
def wsse_username_token(xml)
  xml[:wsse].UsernameToken do
    xml[:wsse].Username(@user)
    xml[:wsse].Password(@password)
  end
end
wsu_timestamp(xml, id, datum = nil) click to toggle source
# File lib/rbvmomi/sso.rb, line 282
def wsu_timestamp(xml, id, datum = nil)
  datum ||= Time.now.utc
  created_at = datum.iso8601
  expires_at = (datum + 600).iso8601

  xml[:wsu].Timestamp('wsu:Id' => id) do
    xml[:wsu].Created(created_at)
    xml[:wsu].Expires(expires_at)
  end
end