winrm-s 0.2.4 → 0.3.0.dev.0
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 +4 -4
- data/.gitignore +27 -27
- data/.rspec +2 -2
- data/CHANGELOG.md +29 -29
- data/CONTRIBUTING.md +5 -5
- data/Gemfile +7 -7
- data/LICENSE +201 -201
- data/README.md +69 -69
- data/Rakefile +1 -1
- data/lib/winrm-s.rb +30 -30
- data/lib/winrm-s/version.rb +22 -22
- data/lib/winrm/helpers/assert_patch.rb +17 -17
- data/lib/winrm/http/auth.rb +191 -191
- data/lib/winrm/http/transport_patch.rb +35 -35
- data/lib/winrm/win32/sspi.rb +256 -256
- data/winrm-s.gemspec +25 -25
- metadata +23 -25
@@ -1,35 +1,35 @@
|
|
1
|
-
#
|
2
|
-
# Author:: Kaustubh Deorukhkar (<kaustubh@clogeny.com>)
|
3
|
-
# Copyright:: Copyright (c) 2014 Chef Software, Inc.
|
4
|
-
# License:: Apache License, Version 2.0
|
5
|
-
#
|
6
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
-
# you may not use this file except in compliance with the License.
|
8
|
-
# You may obtain a copy of the License at
|
9
|
-
#
|
10
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
-
#
|
12
|
-
# Unless required by applicable law or agreed to in writing, software
|
13
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
-
# See the License for the specific language governing permissions and
|
16
|
-
# limitations under the License.
|
17
|
-
#
|
18
|
-
|
19
|
-
module WinRM
|
20
|
-
module HTTP
|
21
|
-
|
22
|
-
class HttpSSPINegotiate < HttpTransport
|
23
|
-
def initialize(endpoint, user, pass, opts)
|
24
|
-
# Override the relevant functionality in httpclient to make sspi work.
|
25
|
-
require 'winrm/http/auth'
|
26
|
-
super(endpoint)
|
27
|
-
@httpcli.set_auth(nil, user, pass)
|
28
|
-
# Remove non-sspi auths
|
29
|
-
auths = @httpcli.www_auth.instance_variable_get('@authenticator')
|
30
|
-
auths.delete_if {|i| not i.is_a?(HTTPClient::SSPINegotiateAuth)}
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
end
|
35
|
-
end # WinRM
|
1
|
+
#
|
2
|
+
# Author:: Kaustubh Deorukhkar (<kaustubh@clogeny.com>)
|
3
|
+
# Copyright:: Copyright (c) 2014 Chef Software, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
module WinRM
|
20
|
+
module HTTP
|
21
|
+
|
22
|
+
class HttpSSPINegotiate < HttpTransport
|
23
|
+
def initialize(endpoint, user, pass, opts)
|
24
|
+
# Override the relevant functionality in httpclient to make sspi work.
|
25
|
+
require 'winrm/http/auth'
|
26
|
+
super(endpoint)
|
27
|
+
@httpcli.set_auth(nil, user, pass)
|
28
|
+
# Remove non-sspi auths
|
29
|
+
auths = @httpcli.www_auth.instance_variable_get('@authenticator')
|
30
|
+
auths.delete_if {|i| not i.is_a?(HTTPClient::SSPINegotiateAuth)}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end # WinRM
|
data/lib/winrm/win32/sspi.rb
CHANGED
@@ -1,256 +1,256 @@
|
|
1
|
-
|
2
|
-
require 'winrm/helpers/assert_patch'
|
3
|
-
|
4
|
-
def validate_patch
|
5
|
-
# The code below patches the Win32::SSPI module from ruby core to add support
|
6
|
-
# for encrypt/decrypt as described below.
|
7
|
-
# Add few restrictions to make sure the patched methods are still
|
8
|
-
# available, but still give a way to consciously use later versions
|
9
|
-
PatchAssertions.assert_arity_of_patched_method(Win32::SSPI::NegotiateAuth, "initialize", -1)
|
10
|
-
PatchAssertions.assert_arity_of_patched_method(Win32::SSPI::NegotiateAuth, "complete_authentication", 1)
|
11
|
-
PatchAssertions.assert_arity_of_patched_method(Win32::SSPI::NegotiateAuth, "get_credentials", 0)
|
12
|
-
end
|
13
|
-
|
14
|
-
# Perform the patch validations
|
15
|
-
validate_patch
|
16
|
-
|
17
|
-
# Overrides and enhances the ruby core win32 sspi module to add support to
|
18
|
-
# encrypt/decrypt data to be sent over channel, example using SSP Negotiate auth
|
19
|
-
|
20
|
-
module Win32
|
21
|
-
module SSPI
|
22
|
-
# QueryContextAttributes attributes flags
|
23
|
-
SECPKG_ATTR_SIZES = 0x00000000
|
24
|
-
|
25
|
-
module API
|
26
|
-
QueryContextAttributes = Win32API.new("secur32", "QueryContextAttributes", 'pLp', 'L')
|
27
|
-
EncryptMessage = Win32API.new("secur32", "EncryptMessage", 'pLpL', 'L')
|
28
|
-
DecryptMessage = Win32API.new("secur32", "DecryptMessage", 'ppLp', 'L')
|
29
|
-
end
|
30
|
-
|
31
|
-
class SecPkgContext_Sizes
|
32
|
-
attr_accessor :cbMaxToken, :cbMaxSignature, :cbBlockSize, :cbSecurityTrailer
|
33
|
-
|
34
|
-
def initialize
|
35
|
-
@cbMaxToken = @cbMaxSignature = @cbBlockSize = @cbSecurityTrailer = 0
|
36
|
-
end
|
37
|
-
|
38
|
-
def cbMaxToken
|
39
|
-
@cbMaxToken = @struct.unpack("LLLL")[0] if @struct
|
40
|
-
end
|
41
|
-
|
42
|
-
def cbMaxSignature
|
43
|
-
@cbMaxSignature = @struct.unpack("LLLL")[1] if @struct
|
44
|
-
end
|
45
|
-
|
46
|
-
def cbBlockSize
|
47
|
-
@cbBlockSize = @struct.unpack("LLLL")[2] if @struct
|
48
|
-
end
|
49
|
-
|
50
|
-
def cbSecurityTrailer
|
51
|
-
@cbSecurityTrailer = @struct.unpack("LLLL")[3] if @struct
|
52
|
-
end
|
53
|
-
|
54
|
-
def to_p
|
55
|
-
@struct ||= [@cbMaxToken, @cbMaxSignature, @cbBlockSize, @cbSecurityTrailer].pack("LLLL")
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
# SecurityBuffer for data to be encrypted
|
60
|
-
class EncryptedSecurityBuffer
|
61
|
-
|
62
|
-
SECBUFFER_DATA = 0x1 # Security buffer data
|
63
|
-
SECBUFFER_TOKEN = 0x2 # Security token
|
64
|
-
SECBUFFER_VERSION = 0
|
65
|
-
|
66
|
-
def initialize(data_buffer, sizes)
|
67
|
-
@original_msg_len = data_buffer.length
|
68
|
-
@cbSecurityTrailer = sizes.cbSecurityTrailer
|
69
|
-
@data_buffer = data_buffer
|
70
|
-
@security_trailer = "\0" * sizes.cbSecurityTrailer
|
71
|
-
end
|
72
|
-
|
73
|
-
def to_p
|
74
|
-
# Assumption is that when to_p is called we are going to get a packed structure. Therefore,
|
75
|
-
# set @unpacked back to nil so we know to unpack when accessors are next accessed.
|
76
|
-
@unpacked = nil
|
77
|
-
# Assignment of inner structure to variable is very important here. Without it,
|
78
|
-
# will not be able to unpack changes to the structure. Alternative, nested unpacks,
|
79
|
-
# does not work (i.e. @struct.unpack("LLP12")[2].unpack("LLP12") results in "no associated pointer")
|
80
|
-
@sec_buffer = [@original_msg_len, SECBUFFER_DATA, @data_buffer, @cbSecurityTrailer, SECBUFFER_TOKEN, @security_trailer].pack("LLPLLP")
|
81
|
-
@struct ||= [SECBUFFER_VERSION, 2, @sec_buffer].pack("LLP")
|
82
|
-
end
|
83
|
-
|
84
|
-
def buffer
|
85
|
-
unpack
|
86
|
-
@buffer
|
87
|
-
end
|
88
|
-
|
89
|
-
private
|
90
|
-
|
91
|
-
# Unpacks the SecurityBufferDesc structure into member variables. We
|
92
|
-
# only want to do this once per struct, so the struct is deleted
|
93
|
-
# after unpacking.
|
94
|
-
def unpack
|
95
|
-
if ! @unpacked && @sec_buffer && @struct
|
96
|
-
dataBufferSize, dType, dataBuffer, tokenBufferSize, tType, tokenBuffer = @sec_buffer.unpack("LLPLLP")
|
97
|
-
dataBufferSize, dType, dataBuffer, tokenBufferSize, tType, tokenBuffer = @sec_buffer.unpack("LLP#{dataBufferSize}LLP#{tokenBufferSize}")
|
98
|
-
# Form the buffer stream as required by server
|
99
|
-
@buffer = [tokenBufferSize].pack("L")
|
100
|
-
@buffer << tokenBuffer << dataBuffer
|
101
|
-
@struct = nil
|
102
|
-
@sec_buffer = nil
|
103
|
-
@unpacked = true
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
class DecryptedSecurityBuffer
|
109
|
-
|
110
|
-
SECBUFFER_DATA = 0x1 # Security buffer data
|
111
|
-
SECBUFFER_TOKEN = 0x2 # Security token
|
112
|
-
SECBUFFER_VERSION = 0
|
113
|
-
|
114
|
-
def initialize(buffer)
|
115
|
-
# unpack to extract the msg and token
|
116
|
-
token_size, token_buffer, enc_buffer = buffer.unpack("L")
|
117
|
-
@original_msg_len = buffer.length - token_size - 4
|
118
|
-
@cbSecurityTrailer, @security_trailer, @data_buffer = buffer.unpack("La#{token_size}a#{@original_msg_len}")
|
119
|
-
end
|
120
|
-
|
121
|
-
def to_p
|
122
|
-
# Assumption is that when to_p is called we are going to get a packed structure. Therefore,
|
123
|
-
# set @unpacked back to nil so we know to unpack when accessors are next accessed.
|
124
|
-
@unpacked = nil
|
125
|
-
# Assignment of inner structure to variable is very important here. Without it,
|
126
|
-
# will not be able to unpack changes to the structure. Alternative, nested unpacks,
|
127
|
-
# does not work (i.e. @struct.unpack("LLP12")[2].unpack("LLP12") results in "no associated pointer")
|
128
|
-
@sec_buffer = [@original_msg_len, SECBUFFER_DATA, @data_buffer, @cbSecurityTrailer, SECBUFFER_TOKEN, @security_trailer].pack("LLPLLP")
|
129
|
-
@struct ||= [SECBUFFER_VERSION, 2, @sec_buffer].pack("LLP")
|
130
|
-
end
|
131
|
-
|
132
|
-
def buffer
|
133
|
-
unpack
|
134
|
-
@buffer
|
135
|
-
end
|
136
|
-
|
137
|
-
private
|
138
|
-
|
139
|
-
# Unpacks the SecurityBufferDesc structure into member variables. We
|
140
|
-
# only want to do this once per struct, so the struct is deleted
|
141
|
-
# after unpacking.
|
142
|
-
def unpack
|
143
|
-
if ! @unpacked && @sec_buffer && @struct
|
144
|
-
dataBufferSize, dType, dataBuffer, tokenBufferSize, tType, tokenBuffer = @sec_buffer.unpack("LLPLLP")
|
145
|
-
dataBufferSize, dType, dataBuffer, tokenBufferSize, tType, tokenBuffer = @sec_buffer.unpack("LLP#{dataBufferSize}LLP#{tokenBufferSize}")
|
146
|
-
@buffer = dataBuffer
|
147
|
-
@struct = nil
|
148
|
-
@sec_buffer = nil
|
149
|
-
@unpacked = true
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
class NegotiateAuth
|
155
|
-
# Override to remember password
|
156
|
-
# Creates a new instance ready for authentication as the given user in the given domain.
|
157
|
-
# Defaults to current user and domain as defined by ENV["USERDOMAIN"] and ENV["USERNAME"] if
|
158
|
-
# no arguments are supplied.
|
159
|
-
def initialize(user = nil, domain = nil, password = nil)
|
160
|
-
if user.nil? && domain.nil? && ENV["USERNAME"].nil? && ENV["USERDOMAIN"].nil?
|
161
|
-
raise "A username or domain must be supplied since they cannot be retrieved from the environment"
|
162
|
-
end
|
163
|
-
@user = user || ENV["USERNAME"].dup
|
164
|
-
@domain = domain || ENV["USERDOMAIN"].dup
|
165
|
-
@password = password
|
166
|
-
end
|
167
|
-
|
168
|
-
# Takes a token and gets the next token in the Negotiate authentication chain. Token can be Base64 encoded or not.
|
169
|
-
# The token can include the "Negotiate" header and it will be stripped.
|
170
|
-
# Does not indicate if SEC_I_CONTINUE or SEC_E_OK was returned.
|
171
|
-
# Token returned is Base64 encoded w/ all new lines removed.
|
172
|
-
def complete_authentication(token)
|
173
|
-
raise "This object is no longer usable because its resources have been freed." if @cleaned_up
|
174
|
-
|
175
|
-
# Nil token OK, just set it to empty string
|
176
|
-
token = "" if token.nil?
|
177
|
-
|
178
|
-
if token.include? "Negotiate"
|
179
|
-
# If the Negotiate prefix is passed in, assume we are seeing "Negotiate <token>" and get the token.
|
180
|
-
token = token.split(" ").last
|
181
|
-
end
|
182
|
-
|
183
|
-
if token.include? B64_TOKEN_PREFIX
|
184
|
-
# indicates base64 encoded token
|
185
|
-
token = token.strip.unpack("m")[0]
|
186
|
-
end
|
187
|
-
|
188
|
-
outputBuffer = SecurityBuffer.new
|
189
|
-
result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, @context.to_p, nil,
|
190
|
-
REQUEST_FLAGS, 0, SECURITY_NETWORK_DREP, SecurityBuffer.new(token).to_p, 0,
|
191
|
-
@context.to_p,
|
192
|
-
outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p))
|
193
|
-
|
194
|
-
if result.ok? then
|
195
|
-
@auth_successful = true
|
196
|
-
return encode_token(outputBuffer.token)
|
197
|
-
else
|
198
|
-
raise "Error: #{result.to_s}"
|
199
|
-
end
|
200
|
-
ensure
|
201
|
-
# need to make sure we don't clean up if we've already cleaned up.
|
202
|
-
# XXX - clean up later since encrypt/decrypt needs this
|
203
|
-
# clean_up unless @cleaned_up
|
204
|
-
end
|
205
|
-
|
206
|
-
def encrypt_payload(body)
|
207
|
-
if @auth_successful
|
208
|
-
# Approach - http://msdn.microsoft.com/en-us/magazine/cc301890.aspx
|
209
|
-
sizes = SecPkgContext_Sizes.new
|
210
|
-
result = SSPIResult.new(API::QueryContextAttributes.call(@context.to_p, SECPKG_ATTR_SIZES, sizes.to_p))
|
211
|
-
|
212
|
-
outputBuffer = EncryptedSecurityBuffer.new(body, sizes)
|
213
|
-
result = SSPIResult.new(API::EncryptMessage.call(@context.to_p, 0, outputBuffer.to_p, 0 ))
|
214
|
-
encrypted_msg = outputBuffer.buffer
|
215
|
-
|
216
|
-
body = <<-EOF
|
217
|
-
--Encrypted Boundary\r
|
218
|
-
Content-Type: application/HTTP-SPNEGO-session-encrypted\r
|
219
|
-
OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{encrypted_msg.length - sizes.cbSecurityTrailer - 4}\r
|
220
|
-
--Encrypted Boundary\r
|
221
|
-
Content-Type: application/octet-stream\r
|
222
|
-
#{encrypted_msg}--Encrypted Boundary\r
|
223
|
-
EOF
|
224
|
-
end
|
225
|
-
body
|
226
|
-
end
|
227
|
-
|
228
|
-
def decrypt_payload(body)
|
229
|
-
if @auth_successful
|
230
|
-
|
231
|
-
matched_data = /--Encrypted Boundary\s+Content-Type:\s+application\/HTTP-SPNEGO-session-encrypted\s+OriginalContent:\s+type=\S+Length=(\d+)\s+--Encrypted Boundary\s+Content-Type:\s+application\/octet-stream\s+([\S\s]+)--Encrypted Boundary/.match(body)
|
232
|
-
raise "Error: Unencrypted communication not supported. Please check winrm configuration winrm/config/service AllowUnencrypted flag." if matched_data.nil?
|
233
|
-
|
234
|
-
encrypted_msg = matched_data[2]
|
235
|
-
|
236
|
-
outputBuffer = DecryptedSecurityBuffer.new(encrypted_msg)
|
237
|
-
result = SSPIResult.new(API::DecryptMessage.call(@context.to_p, outputBuffer.to_p, 0, 0 ))
|
238
|
-
body = outputBuffer.buffer
|
239
|
-
end
|
240
|
-
body
|
241
|
-
end
|
242
|
-
|
243
|
-
private
|
244
|
-
# ** Override to add password support
|
245
|
-
# Gets credentials based on user, domain or both. If both are nil, an error occurs
|
246
|
-
def get_credentials
|
247
|
-
@credentials = CredHandle.new
|
248
|
-
ts = TimeStamp.new
|
249
|
-
@identity = Identity.new @user, @domain, @password
|
250
|
-
result = SSPIResult.new(API::AcquireCredentialsHandle.call(nil, "Negotiate", SECPKG_CRED_OUTBOUND, nil, @identity.to_p,
|
251
|
-
nil, nil, @credentials.to_p, ts.to_p))
|
252
|
-
raise "Error acquire credentials: #{result}" unless result.ok?
|
253
|
-
end
|
254
|
-
end
|
255
|
-
end
|
256
|
-
end
|
1
|
+
|
2
|
+
require 'winrm/helpers/assert_patch'
|
3
|
+
|
4
|
+
def validate_patch
|
5
|
+
# The code below patches the Win32::SSPI module from ruby core to add support
|
6
|
+
# for encrypt/decrypt as described below.
|
7
|
+
# Add few restrictions to make sure the patched methods are still
|
8
|
+
# available, but still give a way to consciously use later versions
|
9
|
+
PatchAssertions.assert_arity_of_patched_method(Win32::SSPI::NegotiateAuth, "initialize", -1)
|
10
|
+
PatchAssertions.assert_arity_of_patched_method(Win32::SSPI::NegotiateAuth, "complete_authentication", 1)
|
11
|
+
PatchAssertions.assert_arity_of_patched_method(Win32::SSPI::NegotiateAuth, "get_credentials", 0)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Perform the patch validations
|
15
|
+
validate_patch
|
16
|
+
|
17
|
+
# Overrides and enhances the ruby core win32 sspi module to add support to
|
18
|
+
# encrypt/decrypt data to be sent over channel, example using SSP Negotiate auth
|
19
|
+
|
20
|
+
module Win32
|
21
|
+
module SSPI
|
22
|
+
# QueryContextAttributes attributes flags
|
23
|
+
SECPKG_ATTR_SIZES = 0x00000000
|
24
|
+
|
25
|
+
module API
|
26
|
+
QueryContextAttributes = Win32API.new("secur32", "QueryContextAttributes", 'pLp', 'L')
|
27
|
+
EncryptMessage = Win32API.new("secur32", "EncryptMessage", 'pLpL', 'L')
|
28
|
+
DecryptMessage = Win32API.new("secur32", "DecryptMessage", 'ppLp', 'L')
|
29
|
+
end
|
30
|
+
|
31
|
+
class SecPkgContext_Sizes
|
32
|
+
attr_accessor :cbMaxToken, :cbMaxSignature, :cbBlockSize, :cbSecurityTrailer
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
@cbMaxToken = @cbMaxSignature = @cbBlockSize = @cbSecurityTrailer = 0
|
36
|
+
end
|
37
|
+
|
38
|
+
def cbMaxToken
|
39
|
+
@cbMaxToken = @struct.unpack("LLLL")[0] if @struct
|
40
|
+
end
|
41
|
+
|
42
|
+
def cbMaxSignature
|
43
|
+
@cbMaxSignature = @struct.unpack("LLLL")[1] if @struct
|
44
|
+
end
|
45
|
+
|
46
|
+
def cbBlockSize
|
47
|
+
@cbBlockSize = @struct.unpack("LLLL")[2] if @struct
|
48
|
+
end
|
49
|
+
|
50
|
+
def cbSecurityTrailer
|
51
|
+
@cbSecurityTrailer = @struct.unpack("LLLL")[3] if @struct
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_p
|
55
|
+
@struct ||= [@cbMaxToken, @cbMaxSignature, @cbBlockSize, @cbSecurityTrailer].pack("LLLL")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# SecurityBuffer for data to be encrypted
|
60
|
+
class EncryptedSecurityBuffer
|
61
|
+
|
62
|
+
SECBUFFER_DATA = 0x1 # Security buffer data
|
63
|
+
SECBUFFER_TOKEN = 0x2 # Security token
|
64
|
+
SECBUFFER_VERSION = 0
|
65
|
+
|
66
|
+
def initialize(data_buffer, sizes)
|
67
|
+
@original_msg_len = data_buffer.length
|
68
|
+
@cbSecurityTrailer = sizes.cbSecurityTrailer
|
69
|
+
@data_buffer = data_buffer
|
70
|
+
@security_trailer = "\0" * sizes.cbSecurityTrailer
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_p
|
74
|
+
# Assumption is that when to_p is called we are going to get a packed structure. Therefore,
|
75
|
+
# set @unpacked back to nil so we know to unpack when accessors are next accessed.
|
76
|
+
@unpacked = nil
|
77
|
+
# Assignment of inner structure to variable is very important here. Without it,
|
78
|
+
# will not be able to unpack changes to the structure. Alternative, nested unpacks,
|
79
|
+
# does not work (i.e. @struct.unpack("LLP12")[2].unpack("LLP12") results in "no associated pointer")
|
80
|
+
@sec_buffer = [@original_msg_len, SECBUFFER_DATA, @data_buffer, @cbSecurityTrailer, SECBUFFER_TOKEN, @security_trailer].pack("LLPLLP")
|
81
|
+
@struct ||= [SECBUFFER_VERSION, 2, @sec_buffer].pack("LLP")
|
82
|
+
end
|
83
|
+
|
84
|
+
def buffer
|
85
|
+
unpack
|
86
|
+
@buffer
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# Unpacks the SecurityBufferDesc structure into member variables. We
|
92
|
+
# only want to do this once per struct, so the struct is deleted
|
93
|
+
# after unpacking.
|
94
|
+
def unpack
|
95
|
+
if ! @unpacked && @sec_buffer && @struct
|
96
|
+
dataBufferSize, dType, dataBuffer, tokenBufferSize, tType, tokenBuffer = @sec_buffer.unpack("LLPLLP")
|
97
|
+
dataBufferSize, dType, dataBuffer, tokenBufferSize, tType, tokenBuffer = @sec_buffer.unpack("LLP#{dataBufferSize}LLP#{tokenBufferSize}")
|
98
|
+
# Form the buffer stream as required by server
|
99
|
+
@buffer = [tokenBufferSize].pack("L")
|
100
|
+
@buffer << tokenBuffer << dataBuffer
|
101
|
+
@struct = nil
|
102
|
+
@sec_buffer = nil
|
103
|
+
@unpacked = true
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class DecryptedSecurityBuffer
|
109
|
+
|
110
|
+
SECBUFFER_DATA = 0x1 # Security buffer data
|
111
|
+
SECBUFFER_TOKEN = 0x2 # Security token
|
112
|
+
SECBUFFER_VERSION = 0
|
113
|
+
|
114
|
+
def initialize(buffer)
|
115
|
+
# unpack to extract the msg and token
|
116
|
+
token_size, token_buffer, enc_buffer = buffer.unpack("L")
|
117
|
+
@original_msg_len = buffer.length - token_size - 4
|
118
|
+
@cbSecurityTrailer, @security_trailer, @data_buffer = buffer.unpack("La#{token_size}a#{@original_msg_len}")
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_p
|
122
|
+
# Assumption is that when to_p is called we are going to get a packed structure. Therefore,
|
123
|
+
# set @unpacked back to nil so we know to unpack when accessors are next accessed.
|
124
|
+
@unpacked = nil
|
125
|
+
# Assignment of inner structure to variable is very important here. Without it,
|
126
|
+
# will not be able to unpack changes to the structure. Alternative, nested unpacks,
|
127
|
+
# does not work (i.e. @struct.unpack("LLP12")[2].unpack("LLP12") results in "no associated pointer")
|
128
|
+
@sec_buffer = [@original_msg_len, SECBUFFER_DATA, @data_buffer, @cbSecurityTrailer, SECBUFFER_TOKEN, @security_trailer].pack("LLPLLP")
|
129
|
+
@struct ||= [SECBUFFER_VERSION, 2, @sec_buffer].pack("LLP")
|
130
|
+
end
|
131
|
+
|
132
|
+
def buffer
|
133
|
+
unpack
|
134
|
+
@buffer
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
# Unpacks the SecurityBufferDesc structure into member variables. We
|
140
|
+
# only want to do this once per struct, so the struct is deleted
|
141
|
+
# after unpacking.
|
142
|
+
def unpack
|
143
|
+
if ! @unpacked && @sec_buffer && @struct
|
144
|
+
dataBufferSize, dType, dataBuffer, tokenBufferSize, tType, tokenBuffer = @sec_buffer.unpack("LLPLLP")
|
145
|
+
dataBufferSize, dType, dataBuffer, tokenBufferSize, tType, tokenBuffer = @sec_buffer.unpack("LLP#{dataBufferSize}LLP#{tokenBufferSize}")
|
146
|
+
@buffer = dataBuffer
|
147
|
+
@struct = nil
|
148
|
+
@sec_buffer = nil
|
149
|
+
@unpacked = true
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
class NegotiateAuth
|
155
|
+
# Override to remember password
|
156
|
+
# Creates a new instance ready for authentication as the given user in the given domain.
|
157
|
+
# Defaults to current user and domain as defined by ENV["USERDOMAIN"] and ENV["USERNAME"] if
|
158
|
+
# no arguments are supplied.
|
159
|
+
def initialize(user = nil, domain = nil, password = nil)
|
160
|
+
if user.nil? && domain.nil? && ENV["USERNAME"].nil? && ENV["USERDOMAIN"].nil?
|
161
|
+
raise "A username or domain must be supplied since they cannot be retrieved from the environment"
|
162
|
+
end
|
163
|
+
@user = user || ENV["USERNAME"].dup
|
164
|
+
@domain = domain || ENV["USERDOMAIN"].dup
|
165
|
+
@password = password
|
166
|
+
end
|
167
|
+
|
168
|
+
# Takes a token and gets the next token in the Negotiate authentication chain. Token can be Base64 encoded or not.
|
169
|
+
# The token can include the "Negotiate" header and it will be stripped.
|
170
|
+
# Does not indicate if SEC_I_CONTINUE or SEC_E_OK was returned.
|
171
|
+
# Token returned is Base64 encoded w/ all new lines removed.
|
172
|
+
def complete_authentication(token)
|
173
|
+
raise "This object is no longer usable because its resources have been freed." if @cleaned_up
|
174
|
+
|
175
|
+
# Nil token OK, just set it to empty string
|
176
|
+
token = "" if token.nil?
|
177
|
+
|
178
|
+
if token.include? "Negotiate"
|
179
|
+
# If the Negotiate prefix is passed in, assume we are seeing "Negotiate <token>" and get the token.
|
180
|
+
token = token.split(" ").last
|
181
|
+
end
|
182
|
+
|
183
|
+
if token.include? B64_TOKEN_PREFIX
|
184
|
+
# indicates base64 encoded token
|
185
|
+
token = token.strip.unpack("m")[0]
|
186
|
+
end
|
187
|
+
|
188
|
+
outputBuffer = SecurityBuffer.new
|
189
|
+
result = SSPIResult.new(API::InitializeSecurityContext.call(@credentials.to_p, @context.to_p, nil,
|
190
|
+
REQUEST_FLAGS, 0, SECURITY_NETWORK_DREP, SecurityBuffer.new(token).to_p, 0,
|
191
|
+
@context.to_p,
|
192
|
+
outputBuffer.to_p, @contextAttributes, TimeStamp.new.to_p))
|
193
|
+
|
194
|
+
if result.ok? then
|
195
|
+
@auth_successful = true
|
196
|
+
return encode_token(outputBuffer.token)
|
197
|
+
else
|
198
|
+
raise "Error: #{result.to_s}"
|
199
|
+
end
|
200
|
+
ensure
|
201
|
+
# need to make sure we don't clean up if we've already cleaned up.
|
202
|
+
# XXX - clean up later since encrypt/decrypt needs this
|
203
|
+
# clean_up unless @cleaned_up
|
204
|
+
end
|
205
|
+
|
206
|
+
def encrypt_payload(body)
|
207
|
+
if @auth_successful
|
208
|
+
# Approach - http://msdn.microsoft.com/en-us/magazine/cc301890.aspx
|
209
|
+
sizes = SecPkgContext_Sizes.new
|
210
|
+
result = SSPIResult.new(API::QueryContextAttributes.call(@context.to_p, SECPKG_ATTR_SIZES, sizes.to_p))
|
211
|
+
|
212
|
+
outputBuffer = EncryptedSecurityBuffer.new(body, sizes)
|
213
|
+
result = SSPIResult.new(API::EncryptMessage.call(@context.to_p, 0, outputBuffer.to_p, 0 ))
|
214
|
+
encrypted_msg = outputBuffer.buffer
|
215
|
+
|
216
|
+
body = <<-EOF
|
217
|
+
--Encrypted Boundary\r
|
218
|
+
Content-Type: application/HTTP-SPNEGO-session-encrypted\r
|
219
|
+
OriginalContent: type=application/soap+xml;charset=UTF-8;Length=#{encrypted_msg.length - sizes.cbSecurityTrailer - 4}\r
|
220
|
+
--Encrypted Boundary\r
|
221
|
+
Content-Type: application/octet-stream\r
|
222
|
+
#{encrypted_msg}--Encrypted Boundary\r
|
223
|
+
EOF
|
224
|
+
end
|
225
|
+
body
|
226
|
+
end
|
227
|
+
|
228
|
+
def decrypt_payload(body)
|
229
|
+
if @auth_successful
|
230
|
+
|
231
|
+
matched_data = /--Encrypted Boundary\s+Content-Type:\s+application\/HTTP-SPNEGO-session-encrypted\s+OriginalContent:\s+type=\S+Length=(\d+)\s+--Encrypted Boundary\s+Content-Type:\s+application\/octet-stream\s+([\S\s]+)--Encrypted Boundary/.match(body)
|
232
|
+
raise "Error: Unencrypted communication not supported. Please check winrm configuration winrm/config/service AllowUnencrypted flag." if matched_data.nil?
|
233
|
+
|
234
|
+
encrypted_msg = matched_data[2]
|
235
|
+
|
236
|
+
outputBuffer = DecryptedSecurityBuffer.new(encrypted_msg)
|
237
|
+
result = SSPIResult.new(API::DecryptMessage.call(@context.to_p, outputBuffer.to_p, 0, 0 ))
|
238
|
+
body = outputBuffer.buffer
|
239
|
+
end
|
240
|
+
body
|
241
|
+
end
|
242
|
+
|
243
|
+
private
|
244
|
+
# ** Override to add password support
|
245
|
+
# Gets credentials based on user, domain or both. If both are nil, an error occurs
|
246
|
+
def get_credentials
|
247
|
+
@credentials = CredHandle.new
|
248
|
+
ts = TimeStamp.new
|
249
|
+
@identity = Identity.new @user, @domain, @password
|
250
|
+
result = SSPIResult.new(API::AcquireCredentialsHandle.call(nil, "Negotiate", SECPKG_CRED_OUTBOUND, nil, @identity.to_p,
|
251
|
+
nil, nil, @credentials.to_p, ts.to_p))
|
252
|
+
raise "Error acquire credentials: #{result}" unless result.ok?
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|