1# 2# = win32/sspi.rb 3# 4# Copyright (c) 2006-2007 Justin Bailey 5# 6# Written and maintained by Justin Bailey <jgbailey@gmail.com>. 7# 8# This program is free software. You can re-distribute and/or 9# modify this program under the same terms of ruby itself --- 10# Ruby Distribution License or GNU General Public License. 11# 12 13require 'Win32API' 14 15# Implements bindings to Win32 SSPI functions, focused on authentication to a proxy server over HTTP. 16module Win32 17 module SSPI 18 # Specifies how credential structure requested will be used. Only SECPKG_CRED_OUTBOUND is used 19 # here. 20 SECPKG_CRED_INBOUND = 0x00000001 21 SECPKG_CRED_OUTBOUND = 0x00000002 22 SECPKG_CRED_BOTH = 0x00000003 23 24 # Format of token. NETWORK format is used here. 25 SECURITY_NATIVE_DREP = 0x00000010 26 SECURITY_NETWORK_DREP = 0x00000000 27 28 # InitializeSecurityContext Requirement flags 29 ISC_REQ_REPLAY_DETECT = 0x00000004 30 ISC_REQ_SEQUENCE_DETECT = 0x00000008 31 ISC_REQ_CONFIDENTIALITY = 0x00000010 32 ISC_REQ_USE_SESSION_KEY = 0x00000020 33 ISC_REQ_PROMPT_FOR_CREDS = 0x00000040 34 ISC_REQ_CONNECTION = 0x00000800 35 36 # Win32 API Functions. Uses Win32API to bind methods to constants contained in class. 37 module API 38 # Can be called with AcquireCredentialsHandle.call() 39 AcquireCredentialsHandle = Win32API.new("secur32", "AcquireCredentialsHandle", 'ppLpppppp', 'L') 40 # Can be called with InitializeSecurityContext.call() 41 InitializeSecurityContext = Win32API.new("secur32", "InitializeSecurityContext", 'pppLLLpLpppp', 'L') 42 # Can be called with DeleteSecurityContext.call() 43 DeleteSecurityContext = Win32API.new("secur32", "DeleteSecurityContext", 'P', 'L') 44 # Can be called with FreeCredentialsHandle.call() 45 FreeCredentialsHandle = Win32API.new("secur32", "FreeCredentialsHandle", 'P', 'L') 46 end 47 48 # SecHandle struct 49 class SecurityHandle 50 def upper 51 @struct.unpack("LL")[1] 52 end 53 54 def lower 55 @struct.unpack("LL")[0] 56 end 57 58 def to_p 59 @struct ||= "\0" * 8 60 end 61 end 62 63 # Some familiar aliases for the SecHandle structure 64 CredHandle = CtxtHandle = SecurityHandle 65 66 # TimeStamp struct 67 class TimeStamp 68 attr_reader :struct 69 70 def to_p 71 @struct ||= "\0" * 8 72 end 73 end 74 75 # Creates binary representaiton of a SecBufferDesc structure, 76 # including the SecBuffer contained inside. 77 class SecurityBuffer 78 79 SECBUFFER_TOKEN = 2 # Security token 80 81 TOKENBUFSIZE = 12288 82 SECBUFFER_VERSION = 0 83 84 def initialize(buffer = nil) 85 @buffer = buffer || "\0" * TOKENBUFSIZE 86 @bufferSize = @buffer.length 87 @type = SECBUFFER_TOKEN 88 end 89 90 def bufferSize 91 unpack 92 @bufferSize 93 end 94 95 def bufferType 96 unpack 97 @type 98 end 99 100 def token 101 unpack 102 @buffer 103 end 104 105 def to_p 106 # Assumption is that when to_p is called we are going to get a packed structure. Therefore, 107 # set @unpacked back to nil so we know to unpack when accessors are next accessed. 108 @unpacked = nil 109 # Assignment of inner structure to variable is very important here. Without it, 110 # will not be able to unpack changes to the structure. Alternative, nested unpacks, 111 # does not work (i.e. @struct.unpack("LLP12")[2].unpack("LLP12") results in "no associated pointer") 112 @sec_buffer ||= [@bufferSize, @type, @buffer].pack("LLP") 113 @struct ||= [SECBUFFER_VERSION, 1, @sec_buffer].pack("LLP") 114 end 115 116 private 117 118 # Unpacks the SecurityBufferDesc structure into member variables. We 119 # only want to do this once per struct, so the struct is deleted 120 # after unpacking. 121 def unpack 122 if ! @unpacked && @sec_buffer && @struct 123 @bufferSize, @type = @sec_buffer.unpack("LL") 124 @buffer = @sec_buffer.unpack("LLP#{@bufferSize}")[2] 125 @struct = nil 126 @sec_buffer = nil 127 @unpacked = true 128 end 129 end 130 end 131 132 # SEC_WINNT_AUTH_IDENTITY structure 133 class Identity 134 SEC_WINNT_AUTH_IDENTITY_ANSI = 0x1 135 136 attr_accessor :user, :domain, :password 137 138 def initialize(user = nil, domain = nil, password = nil) 139 @user = user 140 @domain = domain 141 @password = password 142 @flags = SEC_WINNT_AUTH_IDENTITY_ANSI 143 end 144 145 def to_p 146 [@user, @user ? @user.length : 0, 147 @domain, @domain ? @domain.length : 0, 148 @password, @password ? @password.length : 0, 149 @flags].pack("PLPLPLL") 150 end 151 end 152 153 # Takes a return result from an SSPI function and interprets the value. 154 class SSPIResult 155 # Good results 156 SEC_E_OK = 0x00000000 157 SEC_I_CONTINUE_NEEDED = 0x00090312 158 159 # These are generally returned by InitializeSecurityContext 160 SEC_E_INSUFFICIENT_MEMORY = 0x80090300 161 SEC_E_INTERNAL_ERROR = 0x80090304 162 SEC_E_INVALID_HANDLE = 0x80090301 163 SEC_E_INVALID_TOKEN = 0x80090308 164 SEC_E_LOGON_DENIED = 0x8009030C 165 SEC_E_NO_AUTHENTICATING_AUTHORITY = 0x80090311 166 SEC_E_NO_CREDENTIALS = 0x8009030E 167 SEC_E_TARGET_UNKNOWN = 0x80090303 168 SEC_E_UNSUPPORTED_FUNCTION = 0x80090302 169 SEC_E_WRONG_PRINCIPAL = 0x80090322 170 171 # These are generally returned by AcquireCredentialsHandle 172 SEC_E_NOT_OWNER = 0x80090306 173 SEC_E_SECPKG_NOT_FOUND = 0x80090305 174 SEC_E_UNKNOWN_CREDENTIALS = 0x8009030D 175 176 @@map = {} 177 constants.each { |v| @@map[self.const_get(v.to_s)] = v } 178 179 attr_reader :value 180 181 def initialize(value) 182 # convert to unsigned long 183 value = [value].pack("L").unpack("L").first 184 raise "#{value.to_s(16)} is not a recognized result" unless @@map.has_key? value 185 @value = value 186 end 187 188 def to_s 189 @@map[@value].to_s 190 end 191 192 def ok? 193 @value == SEC_I_CONTINUE_NEEDED || @value == SEC_E_OK 194 end 195 196 def ==(other) 197 if other.is_a?(SSPIResult) 198 @value == other.value 199 elsif other.is_a?(Fixnum) 200 @value == @@map[other] 201 else 202 false 203 end 204 end 205 end 206 207 # Handles "Negotiate" type authentication. Geared towards authenticating with a proxy server over HTTP 208 class NegotiateAuth 209 attr_accessor :credentials, :context, :contextAttributes, :user, :domain 210 211 # Default request flags for SSPI functions 212 REQUEST_FLAGS = ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION 213 214 # NTLM tokens start with this header always. Encoding alone adds "==" and newline, so remove those 215 B64_TOKEN_PREFIX = ["NTLMSSP"].pack("m").delete("=\n") 216 217 # Given a connection and a request path, performs authentication as the current user and returns 218 # the response from a GET request. The connnection should be a Net::HTTP object, and it should 219 # have been constructed using the Net::HTTP.Proxy method, but anything that responds to "get" will work. 220 # If a user and domain are given, will authenticate as the given user. 221 # Returns the response received from the get method (usually Net::HTTPResponse) 222 def NegotiateAuth.proxy_auth_get(http, path, user = nil, domain = nil) 223 raise "http must respond to :get" unless http.respond_to?(:get) 224 nego_auth = self.new user, domain 225 226 resp = http.get path, { "Proxy-Authorization" => "Negotiate " + nego_auth.get_initial_token } 227 if resp["Proxy-Authenticate"] 228 resp = http.get path, { "Proxy-Authorization" => "Negotiate " + nego_auth.complete_authentication(resp["Proxy-Authenticate"].split(" ").last.strip) } 229 end 230 231 resp 232 end 233 234 # Creates a new instance ready for authentication as the given user in the given domain. 235 # Defaults to current user and domain as defined by ENV["USERDOMAIN"] and ENV["USERNAME"] if 236 # no arguments are supplied. 237 def initialize(user = nil, domain = nil) 238 if user.nil? && domain.nil? && ENV["USERNAME"].nil? && ENV["USERDOMAIN"].nil? 239 raise "A username or domain must be supplied since they cannot be retrieved from the environment" 240 end 241 242 @user = user || ENV["USERNAME"] 243 @domain = domain || ENV["USERDOMAIN"] 244 end 245 246 # Gets the initial Negotiate token. Returns it as a base64 encoded string suitable for use in HTTP. Can 247 # be easily decoded, however. 248 def get_initial_token 249 raise "This object is no longer usable because its resources have been freed." if @cleaned_up 250 get_credentials 251 252 outputBuffer = SecurityBuffer.new 253 @context = CtxtHandle.new 254 @contextAttributes = "\0" * 4 255 256 result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, nil, nil, 257 REQUEST_FLAGS,0, SECURITY_NETWORK_DREP, nil, 0, @context.to_p, outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p)) 258 259 if result.ok? then 260 return encode_token(outputBuffer.token) 261 else 262 raise "Error: #{result.to_s}" 263 end 264 end 265 266 # Takes a token and gets the next token in the Negotiate authentication chain. Token can be Base64 encoded or not. 267 # The token can include the "Negotiate" header and it will be stripped. 268 # Does not indicate if SEC_I_CONTINUE or SEC_E_OK was returned. 269 # Token returned is Base64 encoded w/ all new lines removed. 270 def complete_authentication(token) 271 raise "This object is no longer usable because its resources have been freed." if @cleaned_up 272 273 # Nil token OK, just set it to empty string 274 token = "" if token.nil? 275 276 if token.include? "Negotiate" 277 # If the Negotiate prefix is passed in, assume we are seeing "Negotiate <token>" and get the token. 278 token = token.split(" ").last 279 end 280 281 if token.include? B64_TOKEN_PREFIX 282 # indicates base64 encoded token 283 token = token.strip.unpack("m")[0] 284 end 285 286 outputBuffer = SecurityBuffer.new 287 result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, @context.to_p, nil, 288 REQUEST_FLAGS, 0, SECURITY_NETWORK_DREP, SecurityBuffer.new(token).to_p, 0, 289 @context.to_p, 290 outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p)) 291 292 if result.ok? then 293 return encode_token(outputBuffer.token) 294 else 295 raise "Error: #{result.to_s}" 296 end 297 ensure 298 # need to make sure we don't clean up if we've already cleaned up. 299 clean_up unless @cleaned_up 300 end 301 302 private 303 304 def clean_up 305 # free structures allocated 306 @cleaned_up = true 307 API::FreeCredentialsHandle.call(@credentials.to_p) 308 API::DeleteSecurityContext.call(@context.to_p) 309 @context = nil 310 @credentials = nil 311 @contextAttributes = nil 312 end 313 314 # Gets credentials based on user, domain or both. If both are nil, an error occurs 315 def get_credentials 316 @credentials = CredHandle.new 317 ts = TimeStamp.new 318 @identity = Identity.new @user, @domain 319 result = SSPIResult.new(API::AcquireCredentialsHandle.call(nil, "Negotiate", SECPKG_CRED_OUTBOUND, nil, @identity.to_p, 320 nil, nil, @credentials.to_p, ts.to_p)) 321 raise "Error acquire credentials: #{result}" unless result.ok? 322 end 323 324 def encode_token(t) 325 # encode64 will add newlines every 60 characters so we need to remove those. 326 [t].pack("m").delete("\n") 327 end 328 end 329 end 330end