ruby_smb 3.3.23 → 3.3.24

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d886dad5cf6f498c65899db9f1e92e90ada198ec372944b1342aee7f080a75e
4
- data.tar.gz: f0449b0250fcd31a7828011f0aff42695fe9c42c8bcb1317bacb7290726bacdb
3
+ metadata.gz: 47dd07d1be2ceab9aa605d3664a5fef186ed19dee39e00d3797a992f430c6df2
4
+ data.tar.gz: 68763fac839bb88f6dba2fab538d81492cd5975252012fddd2f875078580dbb5
5
5
  SHA512:
6
- metadata.gz: f905b9bb1f1e300203da9ba5cecc6c171398e88df7dd810a16b6cbe96974412f0ea5c076dfd7af792601944a86d42c93587fdf379ce5e6879bf9761b8d88dacd
7
- data.tar.gz: 2a2f83457861f8dd05f95bb3cc67364bde774ff9190986cf7b2704a1ddafc55e69645a1964e33c42a735db2f89fbfe241073bc4687ed034528830dc026e3e9dd
6
+ metadata.gz: bf4a4ff7e4f3c86289a61f6aabc138046b80b6e8819025cc10f563a5e685238784a387a6ab7cd65abcee82b8b67332e497be3202d99aaeeea744354f22609e84
7
+ data.tar.gz: 7d63f42688208e9ce39785189b14f08eea77c73a32da1b9d967aed4ea4f39a220de7ee2bf6250419c3dd637fb04dbbae807ae81ea1e3d2e36c6676b5ba2b5f9d
@@ -0,0 +1,155 @@
1
+ module RubySMB
2
+ module Gss
3
+ module Provider
4
+ #
5
+ # A GSS provider that advertises Kerberos and surfaces the mechanism token a client sends, without interpreting
6
+ # it.
7
+ #
8
+ # A Kerberos AP-REQ is encrypted to the service the client believes it is talking to, so a server that does not
9
+ # hold that service's key cannot read it. This provider therefore does not attempt to: it hands the token to a
10
+ # handler and lets that decide what to tell the client. That is enough for a server to observe or forward
11
+ # Kerberos authentication, and it keeps Kerberos message parsing out of this library entirely.
12
+ #
13
+ # Accepting Kerberos properly, by decrypting the ticket with a service key and validating the PAC, is a separate
14
+ # concern and is not implemented here.
15
+ #
16
+ # The token handed to the handler is the mechanism token exactly as the client sent it. For Kerberos that is a
17
+ # GSS-API InitialContextToken (RFC 2743 section 3.1), which wraps the mechanism OID and the token identifier
18
+ # around the Kerberos message:
19
+ #
20
+ # 60 82 0c 0e InitialContextToken
21
+ # 06 09 2a 86 48 .. the mechanism OID
22
+ # 01 00 the token id, here KRB_AP_REQ
23
+ # 6e 82 0b fd .. the AP-REQ itself
24
+ #
25
+ # Note that the token id follows the OID rather than starting the token, and that the framing around it is not
26
+ # valid ASN.1, so OpenSSL::ASN1.decode will not parse it. {.token_id} reads it without decoding the payload.
27
+ #
28
+ # @example Capture the token a client sends
29
+ # provider = RubySMB::Gss::Provider::Kerberos.new
30
+ # provider.on_mech_token do |token, authenticator|
31
+ # if RubySMB::Gss::Provider::Kerberos.token_id(token) == RubySMB::Gss::Provider::Kerberos::TOK_ID_KRB_AP_REQ
32
+ # # forward or record the AP-REQ, then decide how to reply
33
+ # end
34
+ # # a handler must return a Result; there is no service key here to validate the ticket, so refuse it
35
+ # RubySMB::Gss::Provider::Result.new(nil, WindowsError::NTStatus::STATUS_LOGON_FAILURE)
36
+ # end
37
+ #
38
+ class Kerberos < Base
39
+ # The GSS token identifiers that may appear in a Kerberos mechanism token, per RFC 4121 section 4.1. They are
40
+ # provided so a handler can tell the messages apart without decoding the payload.
41
+ TOK_ID_KRB_AP_REQ = "\x01\x00".b.freeze
42
+ TOK_ID_KRB_AP_REP = "\x02\x00".b.freeze
43
+ TOK_ID_KRB_ERROR = "\x03\x00".b.freeze
44
+
45
+ #
46
+ # Read the token identifier out of a GSS-API InitialContextToken, so a handler can tell an AP-REQ from an
47
+ # AP-REP or a KRB-ERROR. The identifier follows the mechanism OID rather than starting the token, and the
48
+ # framing is not valid ASN.1, so it is located by walking the lengths rather than by decoding.
49
+ #
50
+ # @param [String] token the mechanism token as received
51
+ # @return [String, nil] the two byte identifier, or nil if the token is not shaped as expected
52
+ def self.token_id(token)
53
+ return nil if token.nil? || token.bytesize < 4 || token.getbyte(0) != 0x60
54
+
55
+ length_byte = token.getbyte(1)
56
+ # a long form length says how many bytes carry the length, a short form is the length itself
57
+ offset = length_byte > 0x80 ? 2 + (length_byte & 0x7f) : 2
58
+ return nil if token.getbyte(offset) != 0x06 # the mechanism OID must follow
59
+
60
+ offset += 2 + token.getbyte(offset + 1)
61
+ token.byteslice(offset, 2)
62
+ end
63
+
64
+ # @param [Proc, nil] block an optional handler for received mechanism tokens, see {#on_mech_token}.
65
+ def initialize(&block)
66
+ @on_mech_token = block
67
+ @allow_anonymous = false
68
+ @allow_guests = false
69
+ end
70
+
71
+ def new_authenticator(server_client)
72
+ Authenticator.new(self, server_client)
73
+ end
74
+
75
+ def mech_types
76
+ # both are advertised because Microsoft clients may select either
77
+ [Gss::OID_KERBEROS_5, Gss::OID_MICROSOFT_KERBEROS_5]
78
+ end
79
+
80
+ #
81
+ # Set or invoke the handler called when a client sends a Kerberos mechanism token.
82
+ #
83
+ # The handler receives the opaque token and the authenticator that received it, and returns the {Result} to
84
+ # reply with. When no handler is set the authentication attempt is rejected, since this provider cannot
85
+ # validate a ticket on its own.
86
+ #
87
+ # @param [String] token the mechanism token, as sent by the client
88
+ # @param [Authenticator] authenticator the authenticator that received it
89
+ # @return [Result, nil]
90
+ def on_mech_token(token=nil, authenticator=nil, &block)
91
+ if block.nil?
92
+ return nil if @on_mech_token.nil?
93
+
94
+ @on_mech_token.call(token, authenticator)
95
+ else
96
+ @on_mech_token = block
97
+ end
98
+ end
99
+
100
+ class Authenticator < Authenticator::Base
101
+ def reset!
102
+ super
103
+ @mech_token = nil
104
+ end
105
+
106
+ # @return [String, nil] the most recent mechanism token received from the client.
107
+ attr_reader :mech_token
108
+
109
+ def process(request_buffer=nil)
110
+ if request_buffer.nil?
111
+ return Result.new(Gss.gss_neg_token_init(@provider.mech_types), WindowsError::NTStatus::STATUS_SUCCESS)
112
+ end
113
+
114
+ token = extract_mech_token(request_buffer)
115
+ if token.nil?
116
+ logger.warn('Received a Kerberos request carrying no mechanism token')
117
+ return
118
+ end
119
+
120
+ @mech_token = token
121
+ result = @provider.on_mech_token(token, self)
122
+ # a handler must return a Result: the session setup path calls nt_status on whatever comes back, so
123
+ # anything else (a missing handler, or a handler that returns e.g. a boolean) is refused here rather
124
+ # than handed on to crash the caller
125
+ return result if result.is_a?(Result)
126
+
127
+ Result.new(nil, WindowsError::NTStatus::STATUS_LOGON_FAILURE)
128
+ end
129
+
130
+ private
131
+
132
+ #
133
+ # Pull the mechanism token out of a SPNEGO NegTokenInit or NegTokenResp. The token is returned exactly as the
134
+ # client sent it, so a caller that forwards it elsewhere does not alter the ticket it contains.
135
+ #
136
+ # @param [String] request_buffer the SPNEGO token as received
137
+ # @return [String, nil]
138
+ def extract_mech_token(request_buffer)
139
+ # the identifier octet tells the two SPNEGO tokens apart: an InitialContextToken carrying a NegTokenInit is
140
+ # tagged [APPLICATION 0], a NegTokenResp continuing an exchange is tagged [CONTEXT 1]
141
+ case request_buffer.b.getbyte(0)
142
+ when 0x60
143
+ SpnegoNegTokenInit.parse(request_buffer).mech_token
144
+ when 0xa1
145
+ SpnegoNegTokenTarg.parse(request_buffer).response_token
146
+ end
147
+ rescue RASN1::ASN1Error => e
148
+ logger.error("Failed to parse the SPNEGO token (#{e.message})")
149
+ nil
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,129 @@
1
+ module RubySMB
2
+ module Gss
3
+ module Provider
4
+ #
5
+ # A GSS provider that offers more than one authentication mechanism to the client and routes each request to
6
+ # whichever of its sub-providers understands the mechanism the client selected.
7
+ #
8
+ # SPNEGO exists so that a client and server can agree on a mechanism, but a server that only ever advertises one
9
+ # has nothing to negotiate. This provider advertises the mechanisms of every provider it holds, in the order they
10
+ # were given, so a client can pick the one it prefers.
11
+ #
12
+ # @example Offer Kerberos, falling back to NTLM
13
+ # provider = RubySMB::Gss::Provider::Multi.new([kerberos_provider, ntlm_provider])
14
+ # RubySMB::Server.new(gss_provider: provider)
15
+ #
16
+ class Multi < Base
17
+ #
18
+ # @param [Array<Provider::Base>] providers the providers to offer, in preference order (most preferred first).
19
+ def initialize(providers)
20
+ raise ArgumentError, 'at least one provider is required' if providers.nil? || providers.empty?
21
+
22
+ @providers = providers.dup.freeze
23
+ end
24
+
25
+ # @return [Array<Provider::Base>] the providers this instance will route between.
26
+ attr_reader :providers
27
+
28
+ def new_authenticator(server_client)
29
+ Authenticator.new(self, server_client)
30
+ end
31
+
32
+ #
33
+ # Every mechanism offered by every provider, in provider order, with duplicates removed so a mechanism supported
34
+ # by two providers is only advertised once.
35
+ #
36
+ # @return [Array<OpenSSL::ASN1::ObjectId>]
37
+ def mech_types
38
+ @providers.flat_map(&:mech_types).uniq(&:oid)
39
+ end
40
+
41
+ #
42
+ # The first provider that handles the specified mechanism, or nil if none do.
43
+ #
44
+ # @param [OpenSSL::ASN1::ObjectId] mech_type the mechanism selected by the client
45
+ # @return [Provider::Base, nil]
46
+ def provider_for(mech_type)
47
+ @providers.find { |provider| provider.supports_mech_type?(mech_type) }
48
+ end
49
+
50
+ def allow_anonymous
51
+ @providers.any?(&:allow_anonymous)
52
+ end
53
+
54
+ def allow_guests
55
+ @providers.any?(&:allow_guests)
56
+ end
57
+
58
+ class Authenticator < Authenticator::Base
59
+ def initialize(provider, server_client)
60
+ # built lazily, so a provider that is advertised but never selected is never instantiated
61
+ @authenticators = {}
62
+ @selected = nil
63
+ super
64
+ end
65
+
66
+ def reset!
67
+ super
68
+ @authenticators&.each_value(&:reset!)
69
+ @selected = nil
70
+ end
71
+
72
+ def process(request_buffer=nil)
73
+ # the advertisement, listing every mechanism the server is willing to accept
74
+ return Result.new(Gss.gss_neg_token_init(@provider.mech_types), WindowsError::NTStatus::STATUS_SUCCESS) if request_buffer.nil?
75
+
76
+ begin
77
+ gss_api = OpenSSL::ASN1.decode(request_buffer)
78
+ rescue OpenSSL::ASN1::ASN1Error => e
79
+ logger.error("Failed to parse the ASN1-encoded authentication request (#{e.message})")
80
+ return
81
+ end
82
+
83
+ if negotiation_init?(gss_api)
84
+ # a NegTokenInit names the mechanism the client chose, so this is where routing is decided
85
+ mech_type = Gss.asn1dig(gss_api, 1, 0, 0, 0, 0)
86
+ authenticator = authenticator_for(mech_type)
87
+ if authenticator.nil?
88
+ logger.warn("Client selected an unsupported GSS mechanism (#{mech_type&.oid || 'unknown'})")
89
+ return
90
+ end
91
+
92
+ @selected = authenticator
93
+ elsif @selected.nil?
94
+ # a NegTokenResp carries no mechanism OID, so it can only be interpreted as a continuation of a
95
+ # negotiation that has already selected one
96
+ logger.warn('Received a GSS continuation token before any mechanism was selected')
97
+ return
98
+ end
99
+
100
+ @selected.process(request_buffer)
101
+ end
102
+
103
+ # The session key belongs to whichever mechanism actually authenticated the client.
104
+ def session_key
105
+ @selected&.session_key
106
+ end
107
+
108
+ def session_key=(value)
109
+ @selected&.session_key = value
110
+ end
111
+
112
+ private
113
+
114
+ # Whether the token is a NegTokenInit, which is the only token that names a mechanism.
115
+ def negotiation_init?(gss_api)
116
+ gss_api&.tag == 0 && gss_api&.tag_class == :APPLICATION
117
+ end
118
+
119
+ def authenticator_for(mech_type)
120
+ provider = @provider.provider_for(mech_type)
121
+ return nil if provider.nil?
122
+
123
+ @authenticators[provider] ||= provider.new_authenticator(@server_client)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -26,26 +26,7 @@ module RubySMB
26
26
 
27
27
  def process(request_buffer=nil)
28
28
  if request_buffer.nil?
29
- # this is only NTLMSSP (as opposed to SPNEGO + NTLMSSP)
30
- buffer = OpenSSL::ASN1::ASN1Data.new([
31
- Gss::OID_SPNEGO,
32
- OpenSSL::ASN1::ASN1Data.new([
33
- OpenSSL::ASN1::Sequence.new([
34
- OpenSSL::ASN1::ASN1Data.new([
35
- OpenSSL::ASN1::Sequence.new([
36
- Gss::OID_NTLMSSP
37
- ])
38
- ], 0, :CONTEXT_SPECIFIC),
39
- OpenSSL::ASN1::ASN1Data.new([
40
- OpenSSL::ASN1::ASN1Data.new([
41
- OpenSSL::ASN1::ASN1Data.new([
42
- OpenSSL::ASN1::GeneralString.new('not_defined_in_RFC4178@please_ignore')
43
- ], 0, :CONTEXT_SPECIFIC)
44
- ], 16, :UNIVERSAL)
45
- ], 3, :CONTEXT_SPECIFIC)
46
- ])
47
- ], 0, :CONTEXT_SPECIFIC)
48
- ], 0, :APPLICATION).to_der
29
+ buffer = Gss.gss_neg_token_init(@provider.mech_types)
49
30
  return Result.new(buffer, WindowsError::NTStatus::STATUS_SUCCESS)
50
31
  end
51
32
 
@@ -293,6 +274,10 @@ module RubySMB
293
274
  Authenticator.new(self, server_client)
294
275
  end
295
276
 
277
+ def mech_types
278
+ [Gss::OID_NTLMSSP]
279
+ end
280
+
296
281
  #
297
282
  # Lookup and return an account based on the username and optionally, the domain. If no domain is specified or
298
283
  # or it is the special value '.', the default domain will be used. The username and domain values are case
@@ -26,6 +26,26 @@ module RubySMB
26
26
  raise NotImplementedError
27
27
  end
28
28
 
29
+ #
30
+ # The GSS mechanisms this provider can handle, in preference order. These are advertised to the client in the
31
+ # SPNEGO NegTokenInit, and are used to route an incoming token to the provider that understands it.
32
+ #
33
+ # @return [Array<OpenSSL::ASN1::ObjectId>]
34
+ def mech_types
35
+ raise NotImplementedError
36
+ end
37
+
38
+ #
39
+ # Whether this provider can handle a token for the specified mechanism.
40
+ #
41
+ # @param [OpenSSL::ASN1::ObjectId] mech_type the mechanism selected by the client
42
+ # @return [Boolean]
43
+ def supports_mech_type?(mech_type)
44
+ return false if mech_type.nil?
45
+
46
+ mech_types.any? { |oid| oid.oid == mech_type.oid }
47
+ end
48
+
29
49
  #
30
50
  # Whether or not anonymous authentication attempts should be permitted.
31
51
  #
@@ -42,3 +62,5 @@ end
42
62
 
43
63
  require 'ruby_smb/gss/provider/authenticator'
44
64
  require 'ruby_smb/gss/provider/ntlm'
65
+ require 'ruby_smb/gss/provider/kerberos'
66
+ require 'ruby_smb/gss/provider/multi'
@@ -0,0 +1,81 @@
1
+ require 'rasn1'
2
+
3
+ module RubySMB
4
+ module Gss
5
+ # The SPNEGO negotiation token an initiator sends first, and that a server sends to advertise the mechanisms it
6
+ # supports. Modelled with RASN1 so the fields can be read and built by name rather than by walking a decoded
7
+ # structure by hand.
8
+ #
9
+ # https://datatracker.ietf.org/doc/html/rfc4178#section-4.2.1
10
+ class MechType < RASN1::Types::ObjectId
11
+ end
12
+
13
+ class MechTypeList < RASN1::Model
14
+ sequence_of(:mech_type, MechType)
15
+ end
16
+
17
+ class ContextFlags < RASN1::Types::BitString
18
+ def initialize(options = {})
19
+ options[:bit_length] = 32
20
+ super
21
+ end
22
+ end
23
+
24
+ # RASN1 does not define a GeneralString type, and a SPNEGO negHints carries its hintName as one, so it is defined
25
+ # here as an octet string tagged UNIVERSAL 27.
26
+ class GeneralString < RASN1::Types::OctetString
27
+ ID = 27
28
+
29
+ def self.type
30
+ 'GeneralString'
31
+ end
32
+ end
33
+
34
+ # NegHints, the optional field Microsoft servers place at [3] of a NegTokenInit2 in lieu of a mechListMIC. Windows
35
+ # servers send a fixed placeholder hintName, so a client that expects the field still finds one.
36
+ #
37
+ # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-spng/8e71cf53-e867-4b79-9df6-cc9edb7f8829
38
+ class NegHints < RASN1::Model
39
+ define_type_accel('general_string', GeneralString)
40
+
41
+ sequence :neg_hints,
42
+ content: [general_string(:hint_name, explicit: 0, class: :context, constructed: true, optional: true),
43
+ octet_string(:hint_address, explicit: 1, class: :context, constructed: true, optional: true)]
44
+ end
45
+
46
+ class NegTokenInit < RASN1::Model
47
+ sequence :neg_token_init, explicit: 0, class: :context, constructed: true,
48
+ content: [wrapper(model(:mech_type_list, MechTypeList), explicit: 0, constructed: true),
49
+ wrapper(model(:context_flags, ContextFlags), explicit: 1, constructed: true, optional: true),
50
+ octet_string(:mech_token, explicit: 2, constructed: true, optional: true),
51
+ wrapper(model(:neg_hints, NegHints), explicit: 3, constructed: true, optional: true)]
52
+ end
53
+
54
+ class SpnegoNegTokenInit < RASN1::Model
55
+ # The placeholder hintName a Windows server sends, reproduced so the advertisement matches what a client expects.
56
+ NEG_HINTS_NAME = 'not_defined_in_RFC4178@please_ignore'.freeze
57
+
58
+ sequence :gssapi, implicit: 0, class: :application, constructed: true,
59
+ content: [objectid(:oid),
60
+ model(:neg_token_init, NegTokenInit)]
61
+
62
+ # Build the NegTokenInit a server sends to advertise the mechanisms it supports, including the Microsoft negHints
63
+ # placeholder so the token is shaped as a Windows server's is.
64
+ #
65
+ # @param [Array<OpenSSL::ASN1::ObjectId>] mech_types the mechanisms to advertise, in preference order.
66
+ # @return [String] the DER encoded token.
67
+ def self.build(mech_types)
68
+ token = new
69
+ token[:gssapi][:oid].value = Gss::OID_SPNEGO.oid
70
+ token[:gssapi][:neg_token_init][:mech_type_list][:mech_type] = mech_types.map { |mech| MechType.new(value: mech.oid) }
71
+ token[:gssapi][:neg_token_init][:neg_hints][:hint_name] = NEG_HINTS_NAME
72
+ token.to_der
73
+ end
74
+
75
+ # @return [String, nil] the mechanism token the initiator carried, or nil if it carried none.
76
+ def mech_token
77
+ self[:gssapi][:neg_token_init][:mech_token].value
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,27 @@
1
+ require 'rasn1'
2
+
3
+ module RubySMB
4
+ module Gss
5
+ # The SPNEGO negotiation token exchanged after the first, carrying a continuation of the selected mechanism.
6
+ # A client sends one to continue an exchange, so it is where a mechanism token arrives on any leg past the first.
7
+ #
8
+ # https://www.rfc-editor.org/rfc/rfc2478
9
+ class SpnegoNegTokenTarg < RASN1::Model
10
+ NEG_RESULTS = { 'accept-completed' => 0,
11
+ 'accept-incomplete' => 1,
12
+ 'reject' => 2,
13
+ 'request-mic' => 3 }.freeze
14
+
15
+ sequence :token, explicit: 1, class: :context, constructed: true,
16
+ content: [enumerated(:neg_result, enum: NEG_RESULTS, explicit: 0, class: :context, constructed: true, optional: true),
17
+ objectid(:supported_mech, explicit: 1, class: :context, constructed: true, optional: true),
18
+ octet_string(:response_token, explicit: 2, class: :context, constructed: true, optional: true),
19
+ octet_string(:mech_list_mic, explicit: 3, class: :context, constructed: true, optional: true)]
20
+
21
+ # @return [String, nil] the mechanism token the continuation carried, or nil if it carried none.
22
+ def response_token
23
+ self[:response_token].value
24
+ end
25
+ end
26
+ end
27
+ end
data/lib/ruby_smb/gss.rb CHANGED
@@ -2,11 +2,18 @@ module RubySMB
2
2
  # module containing methods required for using the [GSS-API](http://www.rfc-editor.org/rfc/rfc2743.txt)
3
3
  # for Secure Protected Negotiation(SPNEGO) in SMB Authentication.
4
4
  module Gss
5
+ require 'ruby_smb/gss/spnego_neg_token_init'
6
+ require 'ruby_smb/gss/spnego_neg_token_targ'
5
7
  require 'ruby_smb/gss/provider'
6
8
 
7
9
  OID_SPNEGO = OpenSSL::ASN1::ObjectId.new('1.3.6.1.5.5.2')
8
10
  OID_NEGOEX = OpenSSL::ASN1::ObjectId.new('1.3.6.1.4.1.311.2.2.30')
9
11
  OID_NTLMSSP = OpenSSL::ASN1::ObjectId.new('1.3.6.1.4.1.311.2.2.10')
12
+ # The Kerberos v5 GSS-API mechanism (RFC 4121). Microsoft's SPNEGO
13
+ # implementation also uses a legacy OID that differs by a single arc, and
14
+ # clients may offer or select either, so both are defined here.
15
+ OID_KERBEROS_5 = OpenSSL::ASN1::ObjectId.new('1.2.840.113554.1.2.2')
16
+ OID_MICROSOFT_KERBEROS_5 = OpenSSL::ASN1::ObjectId.new('1.2.840.48018.1.2.2')
10
17
 
11
18
  # Allow safe navigation of a decoded ASN.1 data structure. Similar to Ruby's
12
19
  # builtin Hash#dig method but using the #value attribute of each ASN object.
@@ -46,6 +53,22 @@ module RubySMB
46
53
  encoded_string
47
54
  end
48
55
 
56
+ # Build the SPNEGO NegTokenInit that a server sends to advertise the
57
+ # authentication mechanisms it supports, per RFC 4178 section 4.2.1.
58
+ #
59
+ # The mechTypes list is supplied by the caller so that it reflects every
60
+ # mechanism the server actually offers, rather than being fixed to a single
61
+ # mechanism by whichever provider happens to build the token.
62
+ #
63
+ # @param [Array<OpenSSL::ASN1::ObjectId>] mech_types the mechanisms to
64
+ # advertise, in preference order (most preferred first).
65
+ # @return [String] the DER encoded NegTokenInit.
66
+ def self.gss_neg_token_init(mech_types)
67
+ raise ArgumentError, 'at least one mechanism must be advertised' if mech_types.nil? || mech_types.empty?
68
+
69
+ SpnegoNegTokenInit.build(mech_types)
70
+ end
71
+
49
72
  # Create a GSS Security Blob of an NTLM Type 1 Message.
50
73
  def self.gss_type1(type1)
51
74
  OpenSSL::ASN1::ASN1Data.new([
@@ -1,3 +1,3 @@
1
1
  module RubySMB
2
- VERSION = '3.3.23'.freeze
2
+ VERSION = '3.3.24'.freeze
3
3
  end
data/ruby_smb.gemspec CHANGED
@@ -43,6 +43,9 @@ Gem::Specification.new do |spec|
43
43
  spec.add_runtime_dependency 'rubyntlm', '>= 0.6.5'
44
44
  spec.add_runtime_dependency 'windows_error', '>= 0.1.4'
45
45
  spec.add_runtime_dependency 'bindata', '2.4.15'
46
+ # 0.12 introduced the model `wrapper` DSL the SPNEGO types rely on; the upper Ruby versions resolve to a
47
+ # newer rasn1, while Ruby 2.7 caps at 0.13.1 (0.14+ needs Ruby 3.0), and both are known good.
48
+ spec.add_runtime_dependency 'rasn1', '>= 0.12'
46
49
  spec.add_runtime_dependency 'openssl-ccm'
47
50
  spec.add_runtime_dependency 'openssl-cmac'
48
51
  end
@@ -0,0 +1,206 @@
1
+ RSpec.describe RubySMB::Gss::Provider::Kerberos do
2
+ let(:server_client) { double('server_client', logger: Logger.new(IO::NULL)) }
3
+ # opaque stand-in for a real AP-REQ; this provider never interprets the payload
4
+ let(:ap_req) { "\x6e\x82\x01\x0a".b + Random.new(1).bytes(64) }
5
+ let(:mech_token) { RubySMB::Gss::Provider::Kerberos::TOK_ID_KRB_AP_REQ + ap_req }
6
+
7
+ subject(:provider) { RubySMB::Gss::Provider::Kerberos.new }
8
+
9
+ describe '#mech_types' do
10
+ it 'advertises both the standard and the Microsoft Kerberos mechanism' do
11
+ expect(provider.mech_types.map(&:oid)).to eq(
12
+ [RubySMB::Gss::OID_KERBEROS_5.oid, RubySMB::Gss::OID_MICROSOFT_KERBEROS_5.oid]
13
+ )
14
+ end
15
+
16
+ it 'reports support for both' do
17
+ expect(provider.supports_mech_type?(RubySMB::Gss::OID_KERBEROS_5)).to be true
18
+ expect(provider.supports_mech_type?(RubySMB::Gss::OID_MICROSOFT_KERBEROS_5)).to be true
19
+ end
20
+
21
+ it 'does not report support for other mechanisms' do
22
+ expect(provider.supports_mech_type?(RubySMB::Gss::OID_NTLMSSP)).to be false
23
+ end
24
+ end
25
+
26
+ describe '.token_id' do
27
+ # a GSS-API InitialContextToken, shaped as a Windows client actually sends one: the token id follows the
28
+ # mechanism OID rather than starting the token, and the framing around it is not valid ASN.1
29
+ let(:initial_context_token) do
30
+ "\x60\x82\x0c\x0e".b +
31
+ OpenSSL::ASN1::ObjectId.new('1.2.840.113554.1.2.2').to_der +
32
+ RubySMB::Gss::Provider::Kerberos::TOK_ID_KRB_AP_REQ +
33
+ "\x6e\x82\x0b\xfd".b
34
+ end
35
+
36
+ it 'reads the identifier from past the mechanism OID' do
37
+ expect(RubySMB::Gss::Provider::Kerberos.token_id(initial_context_token))
38
+ .to eq(RubySMB::Gss::Provider::Kerberos::TOK_ID_KRB_AP_REQ)
39
+ end
40
+
41
+ it 'handles a short form length' do
42
+ short = "\x60\x14".b + OpenSSL::ASN1::ObjectId.new('1.2.840.113554.1.2.2').to_der +
43
+ RubySMB::Gss::Provider::Kerberos::TOK_ID_KRB_AP_REP + "\x6f\x00".b
44
+ expect(RubySMB::Gss::Provider::Kerberos.token_id(short))
45
+ .to eq(RubySMB::Gss::Provider::Kerberos::TOK_ID_KRB_AP_REP)
46
+ end
47
+
48
+ it 'is nil for anything not shaped like an InitialContextToken' do
49
+ expect(RubySMB::Gss::Provider::Kerberos.token_id(nil)).to be_nil
50
+ expect(RubySMB::Gss::Provider::Kerberos.token_id('')).to be_nil
51
+ expect(RubySMB::Gss::Provider::Kerberos.token_id('short')).to be_nil
52
+ # a SEQUENCE rather than an InitialContextToken
53
+ expect(RubySMB::Gss::Provider::Kerberos.token_id("\x30\x82\x00\x05".b)).to be_nil
54
+ end
55
+
56
+ it 'is nil when no mechanism OID follows' do
57
+ expect(RubySMB::Gss::Provider::Kerberos.token_id("\x60\x04\x02\x01\x05\x00".b)).to be_nil
58
+ end
59
+ end
60
+
61
+ describe '#on_mech_token' do
62
+ it 'can be set with a block' do
63
+ provider.on_mech_token { |_token, _authenticator| :handled }
64
+ expect(provider.on_mech_token('token', nil)).to eq(:handled)
65
+ end
66
+
67
+ it 'can be set through the constructor' do
68
+ configured = RubySMB::Gss::Provider::Kerberos.new { |_token, _authenticator| :handled }
69
+ expect(configured.on_mech_token('token', nil)).to eq(:handled)
70
+ end
71
+
72
+ it 'is nil when no handler has been set' do
73
+ expect(provider.on_mech_token('token', nil)).to be_nil
74
+ end
75
+ end
76
+
77
+ # referenced explicitly; described_class would resolve to the authenticator inside this group
78
+ describe RubySMB::Gss::Provider::Kerberos::Authenticator do
79
+ subject(:authenticator) { provider.new_authenticator(server_client) }
80
+
81
+ describe '#process' do
82
+ context 'when building the advertisement' do
83
+ it 'offers the Kerberos mechanisms' do
84
+ expect(authenticator.process(nil).buffer).to eq(RubySMB::Gss.gss_neg_token_init(provider.mech_types))
85
+ end
86
+
87
+ it 'succeeds' do
88
+ expect(authenticator.process(nil).nt_status).to eq(WindowsError::NTStatus::STATUS_SUCCESS)
89
+ end
90
+ end
91
+
92
+ context 'with a mechanism token' do
93
+ it 'passes the token to the handler' do
94
+ received = nil
95
+ provider.on_mech_token { |token, _authenticator| received = token; nil }
96
+ authenticator.process(neg_token_init(mech_token))
97
+ expect(received).to eq(mech_token)
98
+ end
99
+
100
+ it 'does not alter the token, so a forwarded ticket stays valid' do
101
+ received = nil
102
+ provider.on_mech_token { |token, _authenticator| received = token; nil }
103
+ authenticator.process(neg_token_init(mech_token))
104
+ expect(received).to eq(mech_token)
105
+ expect(received[2..]).to eq(ap_req)
106
+ end
107
+
108
+ it 'records the token on the authenticator' do
109
+ authenticator.process(neg_token_init(mech_token))
110
+ expect(authenticator.mech_token).to eq(mech_token)
111
+ end
112
+
113
+ it 'returns whatever the handler decides' do
114
+ expected = RubySMB::Gss::Provider::Result.new(nil, WindowsError::NTStatus::STATUS_SUCCESS)
115
+ provider.on_mech_token { |_token, _authenticator| expected }
116
+ expect(authenticator.process(neg_token_init(mech_token))).to be(expected)
117
+ end
118
+
119
+ it 'refuses the attempt when no handler is set' do
120
+ # nothing here can validate a ticket, so the attempt must not silently succeed
121
+ result = authenticator.process(neg_token_init(mech_token))
122
+ expect(result.nt_status).to eq(WindowsError::NTStatus::STATUS_LOGON_FAILURE)
123
+ end
124
+
125
+ it 'refuses the attempt when the handler returns something that is not a Result' do
126
+ # the session setup path calls nt_status on the result, so a non-Result (e.g. a boolean from a naive
127
+ # handler) must be refused here rather than handed on to crash the caller
128
+ provider.on_mech_token { |_token, _authenticator| true }
129
+ result = authenticator.process(neg_token_init(mech_token))
130
+ expect(result).to be_a(RubySMB::Gss::Provider::Result)
131
+ expect(result.nt_status).to eq(WindowsError::NTStatus::STATUS_LOGON_FAILURE)
132
+ end
133
+
134
+ it 'accepts a token carried in a continuation' do
135
+ received = nil
136
+ provider.on_mech_token { |token, _authenticator| received = token; nil }
137
+ authenticator.process(RubySMB::Gss.gss_type3(mech_token))
138
+ expect(received).to eq(mech_token)
139
+ end
140
+ end
141
+
142
+ context 'with a malformed request' do
143
+ it 'returns nil rather than raising' do
144
+ expect(authenticator.process('not asn1 at all')).to be_nil
145
+ end
146
+
147
+ it 'returns nil when there is no mechanism token' do
148
+ expect(authenticator.process(neg_token_init(nil))).to be_nil
149
+ end
150
+ end
151
+ end
152
+
153
+ describe '#reset!' do
154
+ it 'forgets the recorded token' do
155
+ authenticator.process(neg_token_init(mech_token))
156
+ expect(authenticator.mech_token).to_not be_nil
157
+ authenticator.reset!
158
+ expect(authenticator.mech_token).to be_nil
159
+ end
160
+ end
161
+ end
162
+
163
+ describe 'alongside NTLM' do
164
+ let(:ntlm_provider) { RubySMB::Gss::Provider::NTLM.new.tap { |p| p.put_account('RubySMB', 'password') } }
165
+ let(:multi) { RubySMB::Gss::Provider::Multi.new([provider, ntlm_provider]) }
166
+
167
+ it 'is offered ahead of NTLM' do
168
+ expect(multi.mech_types.map(&:oid)).to eq(
169
+ [
170
+ RubySMB::Gss::OID_KERBEROS_5.oid,
171
+ RubySMB::Gss::OID_MICROSOFT_KERBEROS_5.oid,
172
+ RubySMB::Gss::OID_NTLMSSP.oid
173
+ ]
174
+ )
175
+ end
176
+
177
+ it 'receives the token when a client selects Kerberos' do
178
+ received = nil
179
+ provider.on_mech_token { |token, _authenticator| received = token; nil }
180
+ multi.new_authenticator(server_client).process(neg_token_init(mech_token))
181
+ expect(received).to eq(mech_token)
182
+ end
183
+
184
+ it 'is left alone when a client selects NTLM' do
185
+ received = nil
186
+ provider.on_mech_token { |token, _authenticator| received = token; nil }
187
+ type1 = Net::NTLM::Message::Type1.new.tap { |msg| msg.domain = 'WORKGROUP' }
188
+ result = multi.new_authenticator(server_client).process(RubySMB::Gss.gss_type1(type1.serialize))
189
+ expect(received).to be_nil
190
+ expect(result.nt_status).to eq(WindowsError::NTStatus::STATUS_MORE_PROCESSING_REQUIRED)
191
+ end
192
+ end
193
+
194
+ # Build a SPNEGO NegTokenInit selecting Kerberos and carrying the specified mechanism token.
195
+ def neg_token_init(token)
196
+ inner = [OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::Sequence.new([RubySMB::Gss::OID_KERBEROS_5])], 0, :CONTEXT_SPECIFIC)]
197
+ inner << OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::OctetString.new(token)], 2, :CONTEXT_SPECIFIC) unless token.nil?
198
+
199
+ OpenSSL::ASN1::ASN1Data.new(
200
+ [
201
+ RubySMB::Gss::OID_SPNEGO,
202
+ OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::Sequence.new(inner)], 0, :CONTEXT_SPECIFIC)
203
+ ], 0, :APPLICATION
204
+ ).to_der
205
+ end
206
+ end
@@ -0,0 +1,174 @@
1
+ RSpec.describe RubySMB::Gss::Provider::Multi do
2
+ let(:username) { 'RubySMB' }
3
+ let(:domain) { 'WORKGROUP' }
4
+ let(:password) { 'password' }
5
+ let(:ntlm_provider) do
6
+ RubySMB::Gss::Provider::NTLM.new.tap { |provider| provider.put_account(username, password, domain: domain) }
7
+ end
8
+ let(:other_authenticator) { double('authenticator', process: nil, reset!: nil, session_key: nil) }
9
+ # a stand-in for any non-NTLM mechanism, so the routing can be exercised without a second real provider
10
+ let(:other_provider) do
11
+ authenticator = other_authenticator
12
+ Class.new(RubySMB::Gss::Provider::Base) do
13
+ define_method(:mech_types) do
14
+ [RubySMB::Gss::OID_KERBEROS_5, RubySMB::Gss::OID_MICROSOFT_KERBEROS_5]
15
+ end
16
+
17
+ define_method(:new_authenticator) { |_server_client| authenticator }
18
+ end.new
19
+ end
20
+ let(:server_client) { double('server_client', logger: Logger.new(IO::NULL)) }
21
+
22
+ # referenced explicitly rather than via described_class, which resolves to the authenticator inside the nested group
23
+ subject(:provider) { RubySMB::Gss::Provider::Multi.new([other_provider, ntlm_provider]) }
24
+
25
+ describe '#initialize' do
26
+ it 'requires at least one provider' do
27
+ expect { RubySMB::Gss::Provider::Multi.new([]) }.to raise_error(ArgumentError)
28
+ expect { RubySMB::Gss::Provider::Multi.new(nil) }.to raise_error(ArgumentError)
29
+ end
30
+ end
31
+
32
+ describe '#mech_types' do
33
+ it 'advertises every mechanism of every provider' do
34
+ expect(provider.mech_types.map(&:oid)).to eq(
35
+ [
36
+ RubySMB::Gss::OID_KERBEROS_5.oid,
37
+ RubySMB::Gss::OID_MICROSOFT_KERBEROS_5.oid,
38
+ RubySMB::Gss::OID_NTLMSSP.oid
39
+ ]
40
+ )
41
+ end
42
+
43
+ it 'preserves the order the providers were given in' do
44
+ reversed = RubySMB::Gss::Provider::Multi.new([ntlm_provider, other_provider])
45
+ expect(reversed.mech_types.first.oid).to eq(RubySMB::Gss::OID_NTLMSSP.oid)
46
+ end
47
+
48
+ it 'advertises a mechanism supported by two providers only once' do
49
+ duplicated = RubySMB::Gss::Provider::Multi.new([ntlm_provider, RubySMB::Gss::Provider::NTLM.new])
50
+ expect(duplicated.mech_types.length).to eq(1)
51
+ end
52
+ end
53
+
54
+ describe '#provider_for' do
55
+ it 'finds the provider that handles the mechanism' do
56
+ expect(provider.provider_for(RubySMB::Gss::OID_KERBEROS_5)).to be(other_provider)
57
+ expect(provider.provider_for(RubySMB::Gss::OID_NTLMSSP)).to be(ntlm_provider)
58
+ end
59
+
60
+ it 'is nil when no provider handles the mechanism' do
61
+ expect(provider.provider_for(RubySMB::Gss::OID_NEGOEX)).to be_nil
62
+ end
63
+ end
64
+
65
+ describe RubySMB::Gss::Provider::Multi::Authenticator do
66
+ subject(:authenticator) { provider.new_authenticator(server_client) }
67
+
68
+ describe '#process' do
69
+ context 'when building the advertisement' do
70
+ it 'offers all of the mechanisms' do
71
+ buffer = authenticator.process(nil).buffer
72
+ expect(buffer).to eq(RubySMB::Gss.gss_neg_token_init(provider.mech_types))
73
+ end
74
+
75
+ it 'matches the underlying provider when only one is held' do
76
+ single = described_class.new(RubySMB::Gss::Provider::Multi.new([ntlm_provider]), server_client)
77
+ expect(single.process(nil).buffer).to eq(ntlm_provider.new_authenticator(server_client).process(nil).buffer)
78
+ end
79
+
80
+ it 'succeeds' do
81
+ expect(authenticator.process(nil).nt_status).to eq(WindowsError::NTStatus::STATUS_SUCCESS)
82
+ end
83
+ end
84
+
85
+ context 'when the client selects a mechanism' do
86
+ it 'routes the token to the provider that handles it' do
87
+ expect(other_authenticator).to receive(:process)
88
+ authenticator.process(gss_init(RubySMB::Gss::OID_KERBEROS_5))
89
+ end
90
+
91
+ it 'routes an NTLM token to the NTLM provider' do
92
+ type1 = Net::NTLM::Message::Type1.new.tap { |msg| msg.domain = domain }
93
+ result = authenticator.process(RubySMB::Gss.gss_type1(type1.serialize))
94
+ expect(result.nt_status).to eq(WindowsError::NTStatus::STATUS_MORE_PROCESSING_REQUIRED)
95
+ end
96
+
97
+ it 'refuses a mechanism no provider handles' do
98
+ expect(authenticator.process(gss_init(RubySMB::Gss::OID_NEGOEX))).to be_nil
99
+ end
100
+ end
101
+
102
+ context 'when the client continues an exchange' do
103
+ it 'refuses a continuation before a mechanism has been selected' do
104
+ # a NegTokenResp carries no mechanism OID, so there is nothing to route on
105
+ expect(authenticator.process(RubySMB::Gss.gss_type3('anything'))).to be_nil
106
+ end
107
+ end
108
+
109
+ it 'returns nil for a malformed request' do
110
+ expect(authenticator.process('not asn1 at all')).to be_nil
111
+ end
112
+ end
113
+
114
+ describe 'a complete NTLM exchange' do
115
+ it 'authenticates the same as the NTLM provider on its own' do
116
+ expect(complete_ntlm_exchange(authenticator)).to eq(
117
+ complete_ntlm_exchange(ntlm_provider.new_authenticator(server_client))
118
+ )
119
+ end
120
+
121
+ it 'succeeds for a known account' do
122
+ status, identity = complete_ntlm_exchange(authenticator)
123
+ expect(status).to eq(WindowsError::NTStatus::STATUS_SUCCESS)
124
+ expect(identity).to eq("#{domain}\\#{username}")
125
+ end
126
+
127
+ it 'exposes the session key of the mechanism that authenticated' do
128
+ complete_ntlm_exchange(authenticator)
129
+ expect(authenticator.session_key).to_not be_nil
130
+ end
131
+ end
132
+
133
+ describe '#reset!' do
134
+ it 'forgets the selected mechanism' do
135
+ complete_ntlm_exchange(authenticator)
136
+ authenticator.reset!
137
+ expect(authenticator.session_key).to be_nil
138
+ # with no mechanism selected, a continuation token has nothing to route to
139
+ expect(authenticator.process(RubySMB::Gss.gss_type3('anything'))).to be_nil
140
+ end
141
+ end
142
+ end
143
+
144
+ # Build a NegTokenInit that selects the specified mechanism, with an empty mechToken.
145
+ def gss_init(mech_type)
146
+ OpenSSL::ASN1::ASN1Data.new(
147
+ [
148
+ RubySMB::Gss::OID_SPNEGO,
149
+ OpenSSL::ASN1::ASN1Data.new(
150
+ [
151
+ OpenSSL::ASN1::Sequence.new(
152
+ [
153
+ OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::Sequence.new([mech_type])], 0, :CONTEXT_SPECIFIC),
154
+ OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::OctetString.new('')], 2, :CONTEXT_SPECIFIC)
155
+ ]
156
+ )
157
+ ], 0, :CONTEXT_SPECIFIC
158
+ )
159
+ ], 0, :APPLICATION
160
+ ).to_der
161
+ end
162
+
163
+ # Drive a full NTLM negotiation through the authenticator, returning the final status and identity.
164
+ def complete_ntlm_exchange(authenticator)
165
+ authenticator.process(nil)
166
+ type1 = Net::NTLM::Message::Type1.new.tap { |msg| msg.domain = domain }
167
+ challenge_result = authenticator.process(RubySMB::Gss.gss_type1(type1.serialize))
168
+ raw_type2 = RubySMB::Gss.asn1dig(OpenSSL::ASN1.decode(challenge_result.buffer), 0, 2, 0).value
169
+ type2 = Net::NTLM::Message.parse(raw_type2)
170
+ type3 = type2.response({ user: username, password: password, domain: domain }, { ntlmv2: true })
171
+ result = authenticator.process(RubySMB::Gss.gss_type3(type3.serialize))
172
+ [result.nt_status, result.identity]
173
+ end
174
+ end
@@ -0,0 +1,80 @@
1
+ RSpec.describe 'SPNEGO negotiation tokens' do
2
+ let(:mech_types) do
3
+ [RubySMB::Gss::OID_KERBEROS_5, RubySMB::Gss::OID_MICROSOFT_KERBEROS_5, RubySMB::Gss::OID_NTLMSSP]
4
+ end
5
+
6
+ describe RubySMB::Gss::SpnegoNegTokenInit do
7
+ describe '.build' do
8
+ subject(:token) { described_class.build(mech_types) }
9
+
10
+ # the exact bytes a server advertised before this was modelled with RASN1, kept so the wire format does not
11
+ # drift: the SPNEGO OID, the three mechanisms, and the Microsoft negHints placeholder
12
+ let(:legacy_der) do
13
+ [
14
+ '605e06062b0601050502a0543052a024302206092a864886f71201020206092a864882' \
15
+ 'f712010202060a2b06010401823702020aa32a3028a0261b246e6f745f646566696e65' \
16
+ '645f696e5f5246433431373840706c656173655f69676e6f7265'
17
+ ].pack('H*')
18
+ end
19
+
20
+ it 'is byte-identical to the token the hand-rolled builder produced' do
21
+ expect(token).to eq(legacy_der)
22
+ end
23
+
24
+ # the exact NTLM-only advertisement a default server built before this change, when the NTLM provider
25
+ # hardcoded a single OID_NTLMSSP. existing servers still emit this, so lock it against a wire regression.
26
+ it 'is byte-identical to the NTLM-only advertisement a default server built before this change' do
27
+ legacy_ntlm_der = [
28
+ '604806062b0601050502a03e303ca00e300c060a2b06010401823702020aa32a3028' \
29
+ 'a0261b246e6f745f646566696e65645f696e5f5246433431373840706c656173655f' \
30
+ '69676e6f7265'
31
+ ].pack('H*')
32
+ expect(described_class.build([RubySMB::Gss::OID_NTLMSSP])).to eq(legacy_ntlm_der)
33
+ end
34
+
35
+ it 'advertises the mechanisms in order' do
36
+ decoded = OpenSSL::ASN1.decode(token)
37
+ mech_list = decoded.value[1].value[0].value[0].value[0].value
38
+ expect(mech_list.map(&:oid)).to eq(mech_types.map(&:oid))
39
+ end
40
+
41
+ it 'carries the Microsoft negHints placeholder' do
42
+ decoded = OpenSSL::ASN1.decode(token)
43
+ hint = decoded.value[1].value[0].value[1].value[0].value[0].value[0].value
44
+ expect(hint).to eq(RubySMB::Gss::SpnegoNegTokenInit::NEG_HINTS_NAME)
45
+ end
46
+ end
47
+
48
+ describe '.parse' do
49
+ # a SPNEGO NegTokenInit selecting Kerberos and carrying the given mechanism token
50
+ def neg_token_init(token)
51
+ inner = [OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::Sequence.new([RubySMB::Gss::OID_KERBEROS_5])], 0, :CONTEXT_SPECIFIC)]
52
+ inner << OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::OctetString.new(token)], 2, :CONTEXT_SPECIFIC) unless token.nil?
53
+
54
+ OpenSSL::ASN1::ASN1Data.new(
55
+ [
56
+ RubySMB::Gss::OID_SPNEGO,
57
+ OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::Sequence.new(inner)], 0, :CONTEXT_SPECIFIC)
58
+ ], 0, :APPLICATION
59
+ ).to_der
60
+ end
61
+
62
+ it 'reads the mechanism token' do
63
+ expect(described_class.parse(neg_token_init('a mechanism token')).mech_token).to eq('a mechanism token')
64
+ end
65
+
66
+ it 'is nil when the token carries no mechanism token' do
67
+ expect(described_class.parse(neg_token_init(nil)).mech_token).to be_nil
68
+ end
69
+ end
70
+ end
71
+
72
+ describe RubySMB::Gss::SpnegoNegTokenTarg do
73
+ describe '.parse' do
74
+ it 'reads the response token from a continuation' do
75
+ targ = described_class.parse(RubySMB::Gss.gss_type3('a continuation token'))
76
+ expect(targ.response_token).to eq('a continuation token')
77
+ end
78
+ end
79
+ end
80
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_smb
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.23
4
+ version: 3.3.24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Metasploit Hackers
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2026-09-28 00:00:00.000000000 Z
16
+ date: 2026-09-30 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: redcarpet
@@ -127,6 +127,20 @@ dependencies:
127
127
  - - '='
128
128
  - !ruby/object:Gem::Version
129
129
  version: 2.4.15
130
+ - !ruby/object:Gem::Dependency
131
+ name: rasn1
132
+ requirement: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0.12'
137
+ type: :runtime
138
+ prerelease: false
139
+ version_requirements: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0.12'
130
144
  - !ruby/object:Gem::Dependency
131
145
  name: openssl-ccm
132
146
  requirement: !ruby/object:Gem::Requirement
@@ -455,7 +469,11 @@ files:
455
469
  - lib/ruby_smb/gss.rb
456
470
  - lib/ruby_smb/gss/provider.rb
457
471
  - lib/ruby_smb/gss/provider/authenticator.rb
472
+ - lib/ruby_smb/gss/provider/kerberos.rb
473
+ - lib/ruby_smb/gss/provider/multi.rb
458
474
  - lib/ruby_smb/gss/provider/ntlm.rb
475
+ - lib/ruby_smb/gss/spnego_neg_token_init.rb
476
+ - lib/ruby_smb/gss/spnego_neg_token_targ.rb
459
477
  - lib/ruby_smb/impersonation_levels.rb
460
478
  - lib/ruby_smb/nbss.rb
461
479
  - lib/ruby_smb/nbss/negative_session_response.rb
@@ -839,10 +857,13 @@ files:
839
857
  - spec/lib/ruby_smb/fscc/file_system_information_spec.rb
840
858
  - spec/lib/ruby_smb/fscc/fscc_file_attributes_spec.rb
841
859
  - spec/lib/ruby_smb/generic_packet_spec.rb
860
+ - spec/lib/ruby_smb/gss/provider/kerberos_spec.rb
861
+ - spec/lib/ruby_smb/gss/provider/multi_spec.rb
842
862
  - spec/lib/ruby_smb/gss/provider/ntlm/account_spec.rb
843
863
  - spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb
844
864
  - spec/lib/ruby_smb/gss/provider/ntlm/os_version_spec.rb
845
865
  - spec/lib/ruby_smb/gss/provider/ntlm_spec.rb
866
+ - spec/lib/ruby_smb/gss/spnego_spec.rb
846
867
  - spec/lib/ruby_smb/nbss/negative_session_response_spec.rb
847
868
  - spec/lib/ruby_smb/nbss/netbios_name_spec.rb
848
869
  - spec/lib/ruby_smb/nbss/session_header_spec.rb
@@ -1196,10 +1217,13 @@ test_files:
1196
1217
  - spec/lib/ruby_smb/fscc/file_system_information_spec.rb
1197
1218
  - spec/lib/ruby_smb/fscc/fscc_file_attributes_spec.rb
1198
1219
  - spec/lib/ruby_smb/generic_packet_spec.rb
1220
+ - spec/lib/ruby_smb/gss/provider/kerberos_spec.rb
1221
+ - spec/lib/ruby_smb/gss/provider/multi_spec.rb
1199
1222
  - spec/lib/ruby_smb/gss/provider/ntlm/account_spec.rb
1200
1223
  - spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb
1201
1224
  - spec/lib/ruby_smb/gss/provider/ntlm/os_version_spec.rb
1202
1225
  - spec/lib/ruby_smb/gss/provider/ntlm_spec.rb
1226
+ - spec/lib/ruby_smb/gss/spnego_spec.rb
1203
1227
  - spec/lib/ruby_smb/nbss/negative_session_response_spec.rb
1204
1228
  - spec/lib/ruby_smb/nbss/netbios_name_spec.rb
1205
1229
  - spec/lib/ruby_smb/nbss/session_header_spec.rb