win32-sspi 0.0.1.rc1-x86-mingw32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/License.txt +8 -0
- data/README.md +31 -0
- data/Rakefile +49 -0
- data/examples/sspi_negotiate_client.rb +42 -0
- data/examples/sspi_negotiate_server.rb +99 -0
- data/lib/win32-sspi.rb +2 -0
- data/lib/win32/sspi/api/client.rb +17 -0
- data/lib/win32/sspi/api/common.rb +133 -0
- data/lib/win32/sspi/api/server.rb +32 -0
- data/lib/win32/sspi/negotiate/client.rb +159 -0
- data/lib/win32/sspi/negotiate/server.rb +199 -0
- data/lib/win32/sspi/windows/constants.rb +36 -0
- data/lib/win32/sspi/windows/functions.rb +31 -0
- data/lib/win32/sspi/windows/misc.rb +24 -0
- data/lib/win32/sspi/windows/structs.rb +147 -0
- data/test/all_tests.rb +2 -0
- data/test/test_win32_sspi_negotiate_client.rb +417 -0
- data/test/test_win32_sspi_negotiate_server.rb +441 -0
- data/test/test_win32_sspi_structure_creates.rb +100 -0
- data/win32-sspi.gemspec +29 -0
- metadata +102 -0
@@ -0,0 +1,199 @@
|
|
1
|
+
require_relative '../windows/constants'
|
2
|
+
require_relative '../windows/misc'
|
3
|
+
require_relative '../api/server'
|
4
|
+
|
5
|
+
module Win32
|
6
|
+
module SSPI
|
7
|
+
module Negotiate
|
8
|
+
class Server
|
9
|
+
include Windows::Constants
|
10
|
+
include API::Server
|
11
|
+
|
12
|
+
attr_accessor :auth_type
|
13
|
+
attr_reader :token
|
14
|
+
attr_reader :username
|
15
|
+
attr_reader :domain
|
16
|
+
|
17
|
+
def initialize(options={})
|
18
|
+
@auth_type = options[:auth_type] || AUTH_TYPE_NEGOTIATE
|
19
|
+
@token = ""
|
20
|
+
@username = ''
|
21
|
+
@domain = ''
|
22
|
+
@credentials_handle = nil
|
23
|
+
@context_handle = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def http_authenticate(header,&block)
|
27
|
+
perform_authenticate(header,block,true)
|
28
|
+
end
|
29
|
+
|
30
|
+
def authenticate(client_msg,&block)
|
31
|
+
perform_authenticate(client_msg,block,false)
|
32
|
+
end
|
33
|
+
|
34
|
+
def perform_authenticate(client_msg,block,is_http_header)
|
35
|
+
authenticated = false
|
36
|
+
|
37
|
+
status = acquire_handle
|
38
|
+
if SEC_E_OK == status
|
39
|
+
token_from_client_message(client_msg,is_http_header)
|
40
|
+
status = accept_context(self.token)
|
41
|
+
if SEC_I_CONTINUE_NEEDED == status
|
42
|
+
block.call(block_arg_from_token(is_http_header),authenticated)
|
43
|
+
return authenticated
|
44
|
+
end
|
45
|
+
|
46
|
+
if [SEC_I_COMPLETE_NEEDED, SEC_I_COMPLETE_AND_CONTINUE].include?(status)
|
47
|
+
status = complete_authentication
|
48
|
+
end
|
49
|
+
|
50
|
+
if SEC_E_OK == status
|
51
|
+
authenticated = true
|
52
|
+
status = query_attributes
|
53
|
+
if SEC_E_OK == status
|
54
|
+
free_handles
|
55
|
+
end
|
56
|
+
|
57
|
+
block.call(block_arg_from_token(is_http_header),authenticated)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
return authenticated
|
62
|
+
end
|
63
|
+
|
64
|
+
def block_arg_from_token(is_http_header)
|
65
|
+
block_arg = nil
|
66
|
+
if self.token && self.token.length > 0
|
67
|
+
block_arg = is_http_header ? construct_http_header(self.auth_type,self.token) : self.token
|
68
|
+
end
|
69
|
+
block_arg
|
70
|
+
end
|
71
|
+
|
72
|
+
def token_from_client_message(client_msg,is_http_header)
|
73
|
+
if client_msg
|
74
|
+
if is_http_header
|
75
|
+
@auth_type, @token = de_construct_http_header(client_msg)
|
76
|
+
else
|
77
|
+
@token = client_msg
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def acquire_handle
|
83
|
+
return SEC_E_OK if @credentials_handle
|
84
|
+
|
85
|
+
@credentials_handle = create_credhandle
|
86
|
+
expiry = create_timestamp
|
87
|
+
|
88
|
+
status = acquire_credentials_handle(
|
89
|
+
nil,
|
90
|
+
@auth_type,
|
91
|
+
SECPKG_CRED_INBOUND,
|
92
|
+
nil,
|
93
|
+
nil,
|
94
|
+
nil,
|
95
|
+
nil,
|
96
|
+
@credentials_handle,
|
97
|
+
expiry
|
98
|
+
)
|
99
|
+
|
100
|
+
if SEC_E_OK != status
|
101
|
+
@credentials_handle = nil
|
102
|
+
raise SecurityStatusError.new('AcquireCredentialsHandle', status, FFI.errno)
|
103
|
+
end
|
104
|
+
|
105
|
+
status
|
106
|
+
end
|
107
|
+
|
108
|
+
def accept_context(token=nil)
|
109
|
+
ctx = @context_handle
|
110
|
+
@context_handle ||= create_ctxhandle
|
111
|
+
|
112
|
+
if token
|
113
|
+
input_buffer = create_secbuffer(token)
|
114
|
+
input_buffer_desc = create_secbufferdesc(input_buffer)
|
115
|
+
end
|
116
|
+
|
117
|
+
rflags = ASC_REQ_CONFIDENTIALITY | ASC_REQ_REPLAY_DETECT | ASC_REQ_CONNECTION
|
118
|
+
|
119
|
+
output_buffer = create_secbuffer
|
120
|
+
output_buffer_desc = create_secbufferdesc(output_buffer)
|
121
|
+
|
122
|
+
context_attributes = FFI::MemoryPointer.new(:ulong)
|
123
|
+
expiry = create_timestamp
|
124
|
+
|
125
|
+
status = accept_security_context(
|
126
|
+
@credentials_handle,
|
127
|
+
ctx,
|
128
|
+
(token ? input_buffer_desc : nil),
|
129
|
+
rflags,
|
130
|
+
SECURITY_NATIVE_DREP,
|
131
|
+
@context_handle,
|
132
|
+
output_buffer_desc,
|
133
|
+
context_attributes,
|
134
|
+
expiry
|
135
|
+
)
|
136
|
+
|
137
|
+
a_success = [SEC_E_OK, SEC_I_CONTINUE_NEEDED, SEC_I_COMPLETE_NEEDED, SEC_I_COMPLETE_AND_CONTINUE]
|
138
|
+
if a_success.include?(status)
|
139
|
+
@token = output_buffer.to_ruby_s
|
140
|
+
else
|
141
|
+
raise SecurityStatusError.new('AcceptSecurityContext', status, FFI.errno)
|
142
|
+
end
|
143
|
+
|
144
|
+
status
|
145
|
+
end
|
146
|
+
|
147
|
+
def complete_authentication
|
148
|
+
status = SEC_E_OK
|
149
|
+
|
150
|
+
if @token
|
151
|
+
input_buffer = create_secbuffer(@token)
|
152
|
+
input_buffer_desc = create_secbufferdesc(input_buffer)
|
153
|
+
|
154
|
+
status = complete_auth_token(@context_handle, input_buffer_desc)
|
155
|
+
if SEC_E_OK != status
|
156
|
+
raise SecurityStatusError.new('CompleteAuthToken', status, FFI.errno)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
status
|
161
|
+
end
|
162
|
+
|
163
|
+
def query_attributes
|
164
|
+
# Finally, let's get the user and domain
|
165
|
+
ptr = create_secpkg_context_names
|
166
|
+
|
167
|
+
status = query_context_attributes(@context_handle, SECPKG_ATTR_NAMES, ptr)
|
168
|
+
if SEC_E_OK != status
|
169
|
+
raise SecurityStatusError.new('QueryContextAttributes', status, FFI.errno)
|
170
|
+
end
|
171
|
+
|
172
|
+
@username = ptr.to_ruby_s
|
173
|
+
if @username.include?("\\")
|
174
|
+
@domain, @username = @username.split("\\")
|
175
|
+
end
|
176
|
+
|
177
|
+
status = free_context_buffer(ptr.to_username_ptr)
|
178
|
+
if SEC_E_OK != status
|
179
|
+
raise SecurityStatusError.new('FreeContextBuffer', status, FFI.errno)
|
180
|
+
end
|
181
|
+
|
182
|
+
status
|
183
|
+
end
|
184
|
+
|
185
|
+
def free_handles
|
186
|
+
result = free_context_and_credentials(@context_handle, @credentials_handle)
|
187
|
+
@context_handle, @credentials_handle = [nil,nil]
|
188
|
+
|
189
|
+
if SEC_E_OK != result[:status]
|
190
|
+
raise SecurityStatusError.new(result[:name], result[:status], FFI.errno)
|
191
|
+
end
|
192
|
+
|
193
|
+
result[:status]
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Windows
|
2
|
+
module Constants
|
3
|
+
SECURITY_NATIVE_DREP = 0x00000010
|
4
|
+
SECURITY_NETWORK_DREP = 0x00000000
|
5
|
+
SEC_WINNT_AUTH_IDENTITY_ANSI = 1
|
6
|
+
SEC_WINNT_AUTH_IDENTITY_UNICODE = 2
|
7
|
+
SECBUFFER_TOKEN = 2
|
8
|
+
SECBUFFER_VERSION = 0
|
9
|
+
TOKENBUFSIZE = 4096
|
10
|
+
|
11
|
+
SECPKG_ATTR_NAMES = 1
|
12
|
+
SECPKG_CRED_INBOUND = 0x00000001
|
13
|
+
SECPKG_CRED_OUTBOUND = 0x00000002
|
14
|
+
|
15
|
+
ISC_REQ_CONFIDENTIALITY = 0x00000010
|
16
|
+
ISC_REQ_REPLAY_DETECT = 0x00000004
|
17
|
+
ISC_REQ_CONNECTION = 0x00000800
|
18
|
+
|
19
|
+
ASC_REQ_DELEGATE = 0x00000001
|
20
|
+
ASC_REQ_MUTUAL_AUTH = 0x00000002
|
21
|
+
ASC_REQ_REPLAY_DETECT = 0x00000004
|
22
|
+
ASC_REQ_SEQUENCE_DETECT = 0x00000008
|
23
|
+
ASC_REQ_CONFIDENTIALITY = 0x00000010
|
24
|
+
ASC_REQ_CONNECTION = 0x00000800
|
25
|
+
|
26
|
+
SEC_E_OK = 0x00000000
|
27
|
+
SEC_I_CONTINUE_NEEDED = 0x00090312
|
28
|
+
SEC_I_COMPLETE_NEEDED = 0x00090313
|
29
|
+
SEC_I_COMPLETE_AND_CONTINUE = 0x00090314
|
30
|
+
SEC_E_INVALID_HANDLE = 0x80090301
|
31
|
+
SEC_E_INVALID_TOKEN = 0x80090308
|
32
|
+
SEC_E_LOGON_DENIED = 0x8009030C
|
33
|
+
SEC_E_SECPKG_NOT_FOUND = 0x80090305
|
34
|
+
SEC_E_WRONG_PRINCIPAL = 0x80090322
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'ffi'
|
2
|
+
|
3
|
+
module Windows
|
4
|
+
module Functions
|
5
|
+
extend FFI::Library
|
6
|
+
ffi_lib :secur32
|
7
|
+
|
8
|
+
attach_function :AcquireCredentialsHandle, :AcquireCredentialsHandleA,
|
9
|
+
[:string, :string, :ulong, :pointer, :pointer, :pointer, :pointer, :pointer, :pointer],
|
10
|
+
:ulong
|
11
|
+
|
12
|
+
attach_function :InitializeSecurityContext, :InitializeSecurityContextA,
|
13
|
+
[:pointer, :pointer, :string, :ulong, :ulong, :ulong, :pointer, :ulong, :pointer, :pointer, :pointer, :pointer],
|
14
|
+
:ulong
|
15
|
+
|
16
|
+
attach_function :AcceptSecurityContext,
|
17
|
+
[:pointer, :pointer, :pointer, :ulong, :ulong, :pointer, :pointer, :pointer, :pointer],
|
18
|
+
:ulong
|
19
|
+
|
20
|
+
attach_function :CompleteAuthToken, [:pointer, :pointer], :ulong
|
21
|
+
attach_function :DeleteSecurityContext, [:pointer], :ulong
|
22
|
+
attach_function :EnumerateSecurityPackages, :EnumerateSecurityPackagesA, [:pointer, :pointer], :ulong
|
23
|
+
attach_function :FreeContextBuffer, [:pointer], :ulong
|
24
|
+
attach_function :FreeCredentialsHandle, [:pointer], :ulong
|
25
|
+
attach_function :ImpersonateSecurityContext, [:pointer], :ulong
|
26
|
+
attach_function :QueryContextAttributes, :QueryContextAttributesA, [:pointer, :ulong, :pointer], :ulong
|
27
|
+
attach_function :QuerySecurityContextToken, [:pointer, :pointer], :ulong
|
28
|
+
attach_function :QuerySecurityPackageInfo, :QuerySecurityPackageInfoA, [:string, :pointer], :ulong
|
29
|
+
attach_function :RevertSecurityContext, [:pointer], :ulong
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'ffi'
|
2
|
+
require_relative 'constants'
|
3
|
+
|
4
|
+
class SecurityStatusError < StandardError
|
5
|
+
extend FFI::Library
|
6
|
+
|
7
|
+
FORMAT_MESSAGE_FROM_SYSTEM_TABLE = 0x00001000
|
8
|
+
|
9
|
+
ffi_lib :kernel32
|
10
|
+
attach_function :FormatMessageA, [:ulong, :ulong, :ulong, :ulong, :pointer, :ulong, :pointer], :ulong
|
11
|
+
|
12
|
+
def initialize(context,status,errno)
|
13
|
+
hex_status = '0x%X' % status
|
14
|
+
msg = get_return_status_message(status)
|
15
|
+
super("#{context}:\nffi_errno:#{errno} win32_status:#{hex_status}\nwin32 message:#{msg}")
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_return_status_message(win32_return_status)
|
19
|
+
buf = FFI::MemoryPointer.new(:char, 512)
|
20
|
+
flags = FORMAT_MESSAGE_FROM_SYSTEM_TABLE
|
21
|
+
FormatMessageA(flags, 0, win32_return_status, 0, buf, buf.size, nil)
|
22
|
+
buf.read_string
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'ffi'
|
2
|
+
|
3
|
+
module Windows
|
4
|
+
module Structs
|
5
|
+
extend FFI::Library
|
6
|
+
|
7
|
+
class SEC_WINNT_AUTH_IDENTITY < FFI::Struct
|
8
|
+
layout(
|
9
|
+
:User, :pointer,
|
10
|
+
:UserLength, :ulong,
|
11
|
+
:Domain, :pointer,
|
12
|
+
:DomainLength, :ulong,
|
13
|
+
:Password, :pointer,
|
14
|
+
:PasswordLength, :ulong,
|
15
|
+
:Flags, :ulong
|
16
|
+
)
|
17
|
+
|
18
|
+
def user_to_ruby_s
|
19
|
+
bsize = self[:UserLength]
|
20
|
+
bsize > 0 ? self[:User].read_string_length(bsize) : nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def domain_to_ruby_s
|
24
|
+
bsize = self[:DomainLength]
|
25
|
+
bsize > 0 ? self[:Domain].read_string_length(bsize) : nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def password_to_ruby_s
|
29
|
+
bsize = self[:PasswordLength]
|
30
|
+
bsize > 0 ? self[:Password].read_string_length(bsize) : nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class SecHandle < FFI::Struct
|
35
|
+
layout(:dwLower, :pointer, :dwUpper, :pointer)
|
36
|
+
|
37
|
+
# NOTE: Experimental for now, may remove this marshalling stuff later
|
38
|
+
|
39
|
+
def marshal_dump
|
40
|
+
[self[:dwLower].read_ulong, self[:dwUpper].read_ulong]
|
41
|
+
end
|
42
|
+
|
43
|
+
def marshal_load(values)
|
44
|
+
self[:dwLower] = FFI::MemoryPointer.new(:ulong)
|
45
|
+
self[:dwUpper] = FFI::MemoryPointer.new(:ulong)
|
46
|
+
self[:dwLower].write_ulong(values[0])
|
47
|
+
self[:dwUpper].write_ulong(values[1])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
CredHandle = SecHandle
|
52
|
+
CtxtHandle = SecHandle
|
53
|
+
|
54
|
+
class TimeStamp < FFI::Struct
|
55
|
+
layout(:dwLowDateTime, :ulong, :dwHighDateTime, :ulong)
|
56
|
+
|
57
|
+
def marshal_dump
|
58
|
+
[self[:dwLowDateTime], self[:dwHighDateTime]]
|
59
|
+
end
|
60
|
+
|
61
|
+
def marshal_load(values)
|
62
|
+
self[:dwLowDateTime] = values[0]
|
63
|
+
self[:dwHighDateTime] = values[1]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class SecBuffer < FFI::Struct
|
68
|
+
layout(
|
69
|
+
:cbBuffer, :ulong,
|
70
|
+
:BufferType, :ulong,
|
71
|
+
:pvBuffer, :pointer
|
72
|
+
)
|
73
|
+
|
74
|
+
def init(token = nil)
|
75
|
+
self[:BufferType] = 2 # SECBUFFER_TOKEN
|
76
|
+
|
77
|
+
if token
|
78
|
+
self[:cbBuffer] = token.size
|
79
|
+
self[:pvBuffer] = FFI::MemoryPointer.from_string(token)
|
80
|
+
else
|
81
|
+
self[:cbBuffer] = Windows::Constants::TOKENBUFSIZE # Our TOKENBUFSIZE = 4096
|
82
|
+
self[:pvBuffer] = FFI::MemoryPointer.new(:char, Windows::Constants::TOKENBUFSIZE)
|
83
|
+
end
|
84
|
+
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_ruby_s
|
89
|
+
bsize = self[:cbBuffer]
|
90
|
+
bsize > 0 ? self[:pvBuffer].read_string_length(bsize) : nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class SecBufferDesc < FFI::Struct
|
95
|
+
layout(
|
96
|
+
:ulVersion, :ulong,
|
97
|
+
:cBuffers, :ulong,
|
98
|
+
:pBuffers, :pointer
|
99
|
+
)
|
100
|
+
|
101
|
+
def init(sec_buffer)
|
102
|
+
self[:ulVersion] = Windows::Constants::SECBUFFER_VERSION
|
103
|
+
self[:cBuffers] = 1
|
104
|
+
self[:pBuffers] = sec_buffer
|
105
|
+
self
|
106
|
+
end
|
107
|
+
|
108
|
+
def marshal_dump
|
109
|
+
buffer = SecBuffer.new(self[:pBuffers])
|
110
|
+
buffer.to_ruby_s
|
111
|
+
end
|
112
|
+
|
113
|
+
def marshal_load(content)
|
114
|
+
buffer = SecBuffer.new(self[:pBuffers])
|
115
|
+
buffer[:cbBuffer] = content.length
|
116
|
+
buffer[:pvBuffer].write_string(content)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class SecPkgInfo < FFI::Struct
|
121
|
+
layout(
|
122
|
+
:fCapabilities, :ulong,
|
123
|
+
:wVersion, :ushort,
|
124
|
+
:wRPCID, :ushort,
|
125
|
+
:cbMaxToken, :ulong,
|
126
|
+
:Name, :string,
|
127
|
+
:Comment, :string
|
128
|
+
)
|
129
|
+
end
|
130
|
+
|
131
|
+
class SecPkgContext_Names < FFI::Struct
|
132
|
+
layout(:sUserName, :pointer)
|
133
|
+
|
134
|
+
def marshal_load(username)
|
135
|
+
self[:sUserName] = FFI::MemoryPointer.from_string(username)
|
136
|
+
end
|
137
|
+
|
138
|
+
def to_ruby_s
|
139
|
+
self[:sUserName].null? ? nil : self[:sUserName].read_string
|
140
|
+
end
|
141
|
+
|
142
|
+
def to_username_ptr
|
143
|
+
self[:sUserName]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
data/test/all_tests.rb
ADDED
@@ -0,0 +1,417 @@
|
|
1
|
+
########################################################################
|
2
|
+
# Tests for the Win32::SSPI::Negotiate::Client class.
|
3
|
+
########################################################################
|
4
|
+
require 'test-unit'
|
5
|
+
require 'win32/sspi/negotiate/client'
|
6
|
+
|
7
|
+
class TC_Win32_SSPI_Negotiate_Client < Test::Unit::TestCase
|
8
|
+
SPN = "HTTP/virtual-server.gas.local"
|
9
|
+
MockCredentialHandle = [777,888]
|
10
|
+
MockTimeStamp = [0x000000FF,0xFF000000]
|
11
|
+
MockContextHandle = [123,987]
|
12
|
+
MockSecBufferContent = Random.new.bytes(128)
|
13
|
+
ContextAttr = Windows::Constants::ISC_REQ_CONFIDENTIALITY |
|
14
|
+
Windows::Constants::ISC_REQ_REPLAY_DETECT |
|
15
|
+
Windows::Constants::ISC_REQ_CONNECTION
|
16
|
+
|
17
|
+
def setup
|
18
|
+
@client = Win32::SSPI::Negotiate::Client.new(spn:SPN)
|
19
|
+
end
|
20
|
+
|
21
|
+
# assert helper that helps reduce test duplication
|
22
|
+
def assert_client_call_state(client)
|
23
|
+
acquire_args = client.retrieve_state(:acquire)
|
24
|
+
refute_nil acquire_args
|
25
|
+
assert_equal 9, acquire_args.length
|
26
|
+
|
27
|
+
isc_args = client.retrieve_state(:isc)
|
28
|
+
refute_nil isc_args
|
29
|
+
assert_equal 12, isc_args.length
|
30
|
+
|
31
|
+
dsc_args = client.retrieve_state(:dsc)
|
32
|
+
refute_nil dsc_args
|
33
|
+
assert_equal 1, dsc_args.length
|
34
|
+
|
35
|
+
fch_args = client.retrieve_state(:fch)
|
36
|
+
refute_nil fch_args
|
37
|
+
assert_equal 1, fch_args.length
|
38
|
+
|
39
|
+
assert_nil client.instance_variable_get(:@credentials_handle)
|
40
|
+
assert_nil client.instance_variable_get(:@context_handle)
|
41
|
+
end
|
42
|
+
|
43
|
+
def assert_base64_http_header(header,auth_type)
|
44
|
+
if Win32::SSPI::API::Common::AUTH_TYPE_NEGOTIATE == auth_type
|
45
|
+
assert_match( /\ANegotiate \p{Print}+={,2}\z/,header)
|
46
|
+
end
|
47
|
+
if Win32::SSPI::API::Common::AUTH_TYPE_NTLM == auth_type
|
48
|
+
assert_match( /\ANTLM \p{Print}+={,2}\z/,header)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_spn_basic_functionality
|
53
|
+
assert_respond_to(@client, :spn)
|
54
|
+
assert_nothing_raised{ @client.spn }
|
55
|
+
assert_kind_of(String, @client.spn)
|
56
|
+
assert_equal "HTTP/virtual-server.gas.local", @client.spn
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_auth_type_basic_functionality
|
60
|
+
assert_respond_to(@client, :auth_type)
|
61
|
+
assert_nothing_raised{ @client.auth_type }
|
62
|
+
assert_kind_of(String, @client.auth_type)
|
63
|
+
assert_equal Win32::SSPI::API::Common::AUTH_TYPE_NEGOTIATE, @client.auth_type
|
64
|
+
|
65
|
+
client = Win32::SSPI::Negotiate::Client.new(spn:SPN, auth_type:"Kerberos")
|
66
|
+
assert_equal "Kerberos", client.auth_type
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_token_basic_functionality
|
70
|
+
assert_respond_to(@client, :token)
|
71
|
+
assert_nothing_raised{ @client.token }
|
72
|
+
assert_nil @client.token
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_acquire_handle_basic_functionality
|
76
|
+
assert_respond_to(@client, :acquire_handle)
|
77
|
+
assert_equal 0, @client.method(:acquire_handle).arity
|
78
|
+
assert_respond_to(@client, :acquire_credentials_handle)
|
79
|
+
assert_equal 9, @client.method(:acquire_credentials_handle).arity
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_initialize_context_basic_functionality
|
83
|
+
assert_respond_to(@client, :initialize_context)
|
84
|
+
assert_equal( -1, @client.method(:initialize_context).arity)
|
85
|
+
assert_respond_to(@client, :initialize_security_context)
|
86
|
+
assert_equal 12, @client.method(:initialize_security_context).arity
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_acquire_handle_invokes_windows_api_as_expected
|
90
|
+
client = Class.new(MockNegotiateClient).new(spn:SPN)
|
91
|
+
assert_nothing_raised{ @status = client.acquire_handle }
|
92
|
+
assert_equal Windows::Constants::SEC_E_OK, @status
|
93
|
+
|
94
|
+
args = client.retrieve_state(:acquire)
|
95
|
+
assert_equal 9, args.length, "acquire_credentials_handle should have 9 arguments"
|
96
|
+
assert_equal SPN, args[0], "unexpected psz_principal"
|
97
|
+
assert_equal Win32::SSPI::API::Common::AUTH_TYPE_NEGOTIATE, args[1], "unexpected psz_package"
|
98
|
+
assert_equal Windows::Constants::SECPKG_CRED_OUTBOUND, args[2], "unexpected f_credentialuse"
|
99
|
+
assert_nil args[3], "unexpected pv_logonid"
|
100
|
+
assert_nil args[4], "unexpected p_authdata"
|
101
|
+
assert_nil args[5], "unexpected p_getkeyfn"
|
102
|
+
assert_nil args[6], "unexpected p_getkeyarg"
|
103
|
+
assert_kind_of Windows::Structs::CredHandle, args[7], "unexpected ph_newcredentials"
|
104
|
+
assert_equal MockCredentialHandle, args[7].marshal_dump
|
105
|
+
assert_kind_of Windows::Structs::TimeStamp, args[8], "unexpected pts_expiry"
|
106
|
+
assert_equal MockTimeStamp, args[8].marshal_dump
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_acquire_handle_memoizes_handle
|
110
|
+
client = Class.new(MockNegotiateClient).new(spn:SPN)
|
111
|
+
assert_nothing_raised{ client.acquire_handle }
|
112
|
+
assert_nothing_raised{ @status = client.acquire_handle }
|
113
|
+
assert_equal Windows::Constants::SEC_E_OK, @status
|
114
|
+
assert_equal 9, client.retrieve_state(:acquire).length
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_acquire_handle_raises_when_windows_api_returns_failed_status
|
118
|
+
client = Class.new(MockNegotiateClient) do
|
119
|
+
def acquire_credentials_handle(*args)
|
120
|
+
capture_state(:acquire, args)
|
121
|
+
return Windows::Constants::SEC_E_WRONG_PRINCIPAL
|
122
|
+
end
|
123
|
+
end.new(spn:SPN)
|
124
|
+
|
125
|
+
exception = assert_raises(SecurityStatusError){ client.acquire_handle }
|
126
|
+
assert_not_nil exception
|
127
|
+
expected_message = <<-EOM
|
128
|
+
AcquireCredentialsHandle:
|
129
|
+
ffi_errno:0 win32_status:0x80090322
|
130
|
+
win32 message:The target principal name is incorrect.\r
|
131
|
+
EOM
|
132
|
+
assert_equal expected_message, exception.message
|
133
|
+
end
|
134
|
+
|
135
|
+
def test_initialize_context_invokes_windows_api_as_expected
|
136
|
+
client = Class.new(MockNegotiateClient).new(spn:SPN)
|
137
|
+
assert_nothing_raised{ client.acquire_handle }
|
138
|
+
assert_nothing_raised{ @status = client.initialize_context }
|
139
|
+
assert_equal Windows::Constants::SEC_I_CONTINUE_NEEDED, @status
|
140
|
+
|
141
|
+
args = client.retrieve_state(:isc)
|
142
|
+
assert_equal 12, args.length, "unexpected arguments"
|
143
|
+
assert_kind_of Windows::Structs::CredHandle, args[0], "unexpected ph_credentials"
|
144
|
+
assert_equal MockCredentialHandle, args[0].marshal_dump
|
145
|
+
assert_nil args[1], "unexpected ph_context"
|
146
|
+
assert_equal SPN, args[2], "unexpected psz_targetname"
|
147
|
+
|
148
|
+
assert_equal ContextAttr, args[3], "unexpected f_contextreq"
|
149
|
+
assert_equal 0, args[4], "unexpected reserved1"
|
150
|
+
assert_equal Windows::Constants::SECURITY_NETWORK_DREP, args[5], "unexpected targetrep"
|
151
|
+
assert_nil args[6], "unexpected p_input"
|
152
|
+
assert_equal 0, args[7], "unexpected reserved2"
|
153
|
+
assert_kind_of Windows::Structs::CtxtHandle, args[8], "unexpected ph_newcontext"
|
154
|
+
assert_equal MockContextHandle, args[8].marshal_dump
|
155
|
+
assert_kind_of Windows::Structs::SecBufferDesc, args[9], "unexpected p_output"
|
156
|
+
assert_equal MockSecBufferContent, client.token
|
157
|
+
assert_kind_of FFI::MemoryPointer, args[10], "unexpected pf_contextattr"
|
158
|
+
assert_equal ContextAttr, args[10].read_ulong
|
159
|
+
assert_kind_of Windows::Structs::TimeStamp, args[11], "unexpected pts_expiry"
|
160
|
+
assert_equal MockTimeStamp, args[11].marshal_dump
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_initialize_context_raises_when_windows_api_returns_failed_status
|
164
|
+
client = Class.new(MockNegotiateClient) do
|
165
|
+
def initialize_security_context(*args)
|
166
|
+
capture_state(:isc, args)
|
167
|
+
return Windows::Constants::SEC_E_INVALID_TOKEN
|
168
|
+
end
|
169
|
+
end.new(spn:SPN)
|
170
|
+
|
171
|
+
assert_nothing_raised{ client.acquire_handle }
|
172
|
+
exception = assert_raises(SecurityStatusError){ client.initialize_context }
|
173
|
+
assert_not_nil exception
|
174
|
+
expected_message = <<-EOM
|
175
|
+
InitializeSecurityContext:
|
176
|
+
ffi_errno:0 win32_status:0x80090308
|
177
|
+
win32 message:The token supplied to the function is invalid\r
|
178
|
+
EOM
|
179
|
+
assert_equal expected_message, exception.message
|
180
|
+
end
|
181
|
+
|
182
|
+
def test_free_context_and_credentials
|
183
|
+
client = Class.new(MockNegotiateClient).new(spn:SPN)
|
184
|
+
credentials = client.create_credhandle(MockCredentialHandle)
|
185
|
+
context = client.create_ctxhandle(MockContextHandle)
|
186
|
+
result = client.free_context_and_credentials(context,credentials)
|
187
|
+
status_ok = Windows::Constants::SEC_E_OK
|
188
|
+
assert_equal({name:"",status:status_ok,dsc_status:status_ok,fch_status:status_ok}, result)
|
189
|
+
end
|
190
|
+
|
191
|
+
def test_free_context_and_credentials_when_failed_delete
|
192
|
+
client = Class.new(MockNegotiateClient) do
|
193
|
+
def delete_security_context(*args)
|
194
|
+
return Windows::Constants::SEC_E_INVALID_HANDLE
|
195
|
+
end
|
196
|
+
end.new(spn:SPN)
|
197
|
+
credentials = client.create_credhandle(MockCredentialHandle)
|
198
|
+
context = client.create_ctxhandle(MockContextHandle)
|
199
|
+
result = client.free_context_and_credentials(context,credentials)
|
200
|
+
status_ok = Windows::Constants::SEC_E_OK
|
201
|
+
status_failed = Windows::Constants::SEC_E_INVALID_HANDLE
|
202
|
+
expected_result = {name:"DeleteSecurityContext",
|
203
|
+
status:status_failed, dsc_status:status_failed, fch_status:status_ok}
|
204
|
+
assert_equal expected_result, result
|
205
|
+
end
|
206
|
+
|
207
|
+
def test_free_context_and_credentials_when_failed_free
|
208
|
+
client = Class.new(MockNegotiateClient) do
|
209
|
+
def free_credentials_handle(*args)
|
210
|
+
return Windows::Constants::SEC_E_INVALID_HANDLE
|
211
|
+
end
|
212
|
+
end.new(spn:SPN)
|
213
|
+
credentials = client.create_credhandle(MockCredentialHandle)
|
214
|
+
context = client.create_ctxhandle(MockContextHandle)
|
215
|
+
result = client.free_context_and_credentials(context,credentials)
|
216
|
+
status_ok = Windows::Constants::SEC_E_OK
|
217
|
+
status_failed = Windows::Constants::SEC_E_INVALID_HANDLE
|
218
|
+
expected_result = {name:"FreeCredentialsHandle",
|
219
|
+
status:status_failed, dsc_status:status_ok, fch_status:status_failed}
|
220
|
+
assert_equal expected_result, result
|
221
|
+
end
|
222
|
+
|
223
|
+
def test_free_context_and_credentials_when_both_fail
|
224
|
+
client = Class.new(MockNegotiateClient) do
|
225
|
+
def delete_security_context(*args)
|
226
|
+
return Windows::Constants::SEC_E_INVALID_HANDLE
|
227
|
+
end
|
228
|
+
def free_credentials_handle(*args)
|
229
|
+
return Windows::Constants::SEC_E_INVALID_HANDLE
|
230
|
+
end
|
231
|
+
end.new(spn:SPN)
|
232
|
+
credentials = client.create_credhandle(MockCredentialHandle)
|
233
|
+
context = client.create_ctxhandle(MockContextHandle)
|
234
|
+
result = client.free_context_and_credentials(context,credentials)
|
235
|
+
status_failed = Windows::Constants::SEC_E_INVALID_HANDLE
|
236
|
+
expected_result = {name:"FreeCredentialsHandle",
|
237
|
+
status:status_failed, dsc_status:status_failed, fch_status:status_failed}
|
238
|
+
assert_equal expected_result, result
|
239
|
+
end
|
240
|
+
|
241
|
+
def test_both_handles_freed_when_free_handles_raises
|
242
|
+
client = Class.new(MockNegotiateClient) do
|
243
|
+
def delete_security_context(*args)
|
244
|
+
capture_state(:dsc, args)
|
245
|
+
return Windows::Constants::SEC_E_INVALID_TOKEN
|
246
|
+
end
|
247
|
+
end.new(spn:SPN)
|
248
|
+
|
249
|
+
assert_nothing_raised{ client.acquire_handle }
|
250
|
+
assert_nothing_raised{ client.initialize_context }
|
251
|
+
|
252
|
+
refute_nil client.instance_variable_get(:@context_handle)
|
253
|
+
refute_nil client.instance_variable_get(:@credentials_handle)
|
254
|
+
|
255
|
+
assert_raises{ client.free_handles }
|
256
|
+
|
257
|
+
assert_nil client.instance_variable_get(:@context_handle)
|
258
|
+
assert_nil client.instance_variable_get(:@credentials_handle)
|
259
|
+
end
|
260
|
+
|
261
|
+
def test_acquire_handle_invokes_windows_api_as_expected_with_ntlm_auth_type
|
262
|
+
client = Class.new(MockNegotiateClient).new(auth_type:Win32::SSPI::API::Common::AUTH_TYPE_NTLM)
|
263
|
+
assert_nothing_raised{ @status = client.acquire_handle }
|
264
|
+
assert_equal Windows::Constants::SEC_E_OK, @status
|
265
|
+
|
266
|
+
args = client.retrieve_state(:acquire)
|
267
|
+
assert_equal 9, args.length, "acquire_credentials_handle should have 9 arguments"
|
268
|
+
assert_nil args[0], "unexpected psz_principal"
|
269
|
+
assert_equal Win32::SSPI::API::Common::AUTH_TYPE_NTLM, args[1], "unexpected psz_package"
|
270
|
+
assert_equal Windows::Constants::SECPKG_CRED_OUTBOUND, args[2], "unexpected f_credentialuse"
|
271
|
+
assert_nil args[3], "unexpected pv_logonid"
|
272
|
+
assert_kind_of Windows::Structs::SEC_WINNT_AUTH_IDENTITY, args[4], "unexpected p_authdata"
|
273
|
+
assert_equal ENV['USERNAME'], args[4].user_to_ruby_s
|
274
|
+
assert_equal ENV['USERDOMAIN'], args[4].domain_to_ruby_s
|
275
|
+
assert_nil args[5], "unexpected p_getkeyfn"
|
276
|
+
assert_nil args[6], "unexpected p_getkeyarg"
|
277
|
+
assert_kind_of Windows::Structs::CredHandle, args[7], "unexpected ph_newcredentials"
|
278
|
+
assert_equal MockCredentialHandle, args[7].marshal_dump
|
279
|
+
assert_kind_of Windows::Structs::TimeStamp, args[8], "unexpected pts_expiry"
|
280
|
+
assert_equal MockTimeStamp, args[8].marshal_dump
|
281
|
+
end
|
282
|
+
|
283
|
+
def test_acquire_handle_invokes_windows_api_as_expected_with_ntlm_auth_type_and_supplied_credentials
|
284
|
+
client = Class.new(MockNegotiateClient).new(
|
285
|
+
auth_type:Win32::SSPI::API::Common::AUTH_TYPE_NTLM,
|
286
|
+
username:'joey',
|
287
|
+
domain:'local',
|
288
|
+
password:'kskeidksi'
|
289
|
+
)
|
290
|
+
assert_nothing_raised{ @status = client.acquire_handle }
|
291
|
+
assert_equal Windows::Constants::SEC_E_OK, @status
|
292
|
+
|
293
|
+
args = client.retrieve_state(:acquire)
|
294
|
+
assert_equal 9, args.length, "acquire_credentials_handle should have 9 arguments"
|
295
|
+
assert_nil args[0], "unexpected psz_principal"
|
296
|
+
assert_equal Win32::SSPI::API::Common::AUTH_TYPE_NTLM, args[1], "unexpected psz_package"
|
297
|
+
assert_equal Windows::Constants::SECPKG_CRED_OUTBOUND, args[2], "unexpected f_credentialuse"
|
298
|
+
assert_nil args[3], "unexpected pv_logonid"
|
299
|
+
assert_kind_of Windows::Structs::SEC_WINNT_AUTH_IDENTITY, args[4], "unexpected p_authdata"
|
300
|
+
assert_equal 'joey', args[4].user_to_ruby_s
|
301
|
+
assert_equal 'local', args[4].domain_to_ruby_s
|
302
|
+
assert_equal 'kskeidksi', args[4].password_to_ruby_s
|
303
|
+
assert_nil args[5], "unexpected p_getkeyfn"
|
304
|
+
assert_nil args[6], "unexpected p_getkeyarg"
|
305
|
+
assert_kind_of Windows::Structs::CredHandle, args[7], "unexpected ph_newcredentials"
|
306
|
+
assert_equal MockCredentialHandle, args[7].marshal_dump
|
307
|
+
assert_kind_of Windows::Structs::TimeStamp, args[8], "unexpected pts_expiry"
|
308
|
+
assert_equal MockTimeStamp, args[8].marshal_dump
|
309
|
+
end
|
310
|
+
|
311
|
+
def test_http_authenticate
|
312
|
+
client = Class.new(MockNegotiateClient).new(spn:SPN)
|
313
|
+
counter = 0
|
314
|
+
client.http_authenticate do |header|
|
315
|
+
counter += 1
|
316
|
+
fail "loop failed to complete in a reasonable iteration count" if counter > 3
|
317
|
+
assert_base64_http_header(header,Win32::SSPI::API::Common::AUTH_TYPE_NEGOTIATE)
|
318
|
+
header
|
319
|
+
end
|
320
|
+
|
321
|
+
assert_equal 1, counter
|
322
|
+
|
323
|
+
assert_client_call_state(client)
|
324
|
+
end
|
325
|
+
|
326
|
+
def test_http_authenticate_with_ntlm_protocol
|
327
|
+
client = Class.new(MockNegotiateClient).new(auth_type:Win32::SSPI::API::Common::AUTH_TYPE_NTLM)
|
328
|
+
counter = 0
|
329
|
+
client.http_authenticate do |header|
|
330
|
+
counter += 1
|
331
|
+
fail "loop failed to complete in a reasonable iteration count" if counter > 3
|
332
|
+
assert_base64_http_header(header,Win32::SSPI::API::Common::AUTH_TYPE_NTLM)
|
333
|
+
header
|
334
|
+
end
|
335
|
+
|
336
|
+
assert_equal 2, counter
|
337
|
+
|
338
|
+
assert_client_call_state(client)
|
339
|
+
end
|
340
|
+
|
341
|
+
def test_authenticate
|
342
|
+
client = Class.new(MockNegotiateClient).new(spn:SPN)
|
343
|
+
counter = 0
|
344
|
+
client.authenticate do |token|
|
345
|
+
counter += 1
|
346
|
+
fail "loop failed to complete in a reasonable iteration count" if counter > 3
|
347
|
+
assert_equal MockSecBufferContent, token
|
348
|
+
token
|
349
|
+
end
|
350
|
+
|
351
|
+
assert_equal 1, counter
|
352
|
+
|
353
|
+
assert_client_call_state(client)
|
354
|
+
end
|
355
|
+
|
356
|
+
def teardown
|
357
|
+
@client = nil
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
class MockNegotiateClient < Win32::SSPI::Negotiate::Client
|
362
|
+
def acquire_credentials_handle(*args)
|
363
|
+
s_args = retrieve_state(:acquire) || Array.new
|
364
|
+
s_args << args
|
365
|
+
capture_state(:acquire,s_args.flatten)
|
366
|
+
# this api should return a credential handle in arg[7] and a timestamp in arg[8]
|
367
|
+
args[7].marshal_load(TC_Win32_SSPI_Negotiate_Client::MockCredentialHandle)
|
368
|
+
args[8].marshal_load(TC_Win32_SSPI_Negotiate_Client::MockTimeStamp)
|
369
|
+
return Windows::Constants::SEC_E_OK
|
370
|
+
end
|
371
|
+
|
372
|
+
def initialize_security_context(*args)
|
373
|
+
capture_state(:isc, args)
|
374
|
+
status = Windows::Constants::SEC_E_OK
|
375
|
+
status = Windows::Constants::SEC_I_CONTINUE_NEEDED if args[6].nil?
|
376
|
+
# this api should return a new context, p_output, context attr and timestamp
|
377
|
+
args[8].marshal_load(TC_Win32_SSPI_Negotiate_Client::MockContextHandle)
|
378
|
+
args[9].marshal_load(TC_Win32_SSPI_Negotiate_Client::MockSecBufferContent)
|
379
|
+
args[10].write_ulong(TC_Win32_SSPI_Negotiate_Client::ContextAttr)
|
380
|
+
args[11].marshal_load(TC_Win32_SSPI_Negotiate_Client::MockTimeStamp)
|
381
|
+
return status
|
382
|
+
end
|
383
|
+
|
384
|
+
def delete_security_context(*args)
|
385
|
+
capture_state(:dsc,args)
|
386
|
+
return Windows::Constants::SEC_E_OK
|
387
|
+
end
|
388
|
+
|
389
|
+
def free_credentials_handle(*args)
|
390
|
+
capture_state(:fch,args)
|
391
|
+
return Windows::Constants::SEC_E_OK
|
392
|
+
end
|
393
|
+
|
394
|
+
def capture_state(key,value)
|
395
|
+
self.class.capture_state(key,value)
|
396
|
+
end
|
397
|
+
|
398
|
+
def retrieve_state(key)
|
399
|
+
self.class.retrieve_state(key)
|
400
|
+
end
|
401
|
+
|
402
|
+
def self.state
|
403
|
+
@state ||= Hash.new
|
404
|
+
end
|
405
|
+
|
406
|
+
def self.capture_state(key,value)
|
407
|
+
state[key] = value
|
408
|
+
end
|
409
|
+
|
410
|
+
def self.retrieve_state(key)
|
411
|
+
state[key]
|
412
|
+
end
|
413
|
+
|
414
|
+
def self.clear_state
|
415
|
+
state.clear
|
416
|
+
end
|
417
|
+
end
|