crussh 0.1.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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +371 -0
  5. data/ext/poly1305/Cargo.toml +13 -0
  6. data/ext/poly1305/extconf.rb +6 -0
  7. data/ext/poly1305/src/lib.rs +75 -0
  8. data/lib/crussh/auth.rb +46 -0
  9. data/lib/crussh/channel/key_parser.rb +125 -0
  10. data/lib/crussh/channel.rb +381 -0
  11. data/lib/crussh/cipher/algorithm.rb +31 -0
  12. data/lib/crussh/cipher/chacha20poly1305.rb +98 -0
  13. data/lib/crussh/cipher.rb +25 -0
  14. data/lib/crussh/compression.rb +42 -0
  15. data/lib/crussh/gatekeeper.rb +50 -0
  16. data/lib/crussh/handler/line_buffer.rb +131 -0
  17. data/lib/crussh/handler.rb +128 -0
  18. data/lib/crussh/heartbeat.rb +68 -0
  19. data/lib/crussh/kex/algorithm.rb +86 -0
  20. data/lib/crussh/kex/curve25519.rb +30 -0
  21. data/lib/crussh/kex/exchange.rb +234 -0
  22. data/lib/crussh/kex.rb +42 -0
  23. data/lib/crussh/keys/key_pair.rb +61 -0
  24. data/lib/crussh/keys/public_key.rb +35 -0
  25. data/lib/crussh/keys.rb +70 -0
  26. data/lib/crussh/limits.rb +45 -0
  27. data/lib/crussh/logger.rb +95 -0
  28. data/lib/crussh/mac/algorithm.rb +23 -0
  29. data/lib/crussh/mac/crypto.rb +60 -0
  30. data/lib/crussh/mac/none.rb +9 -0
  31. data/lib/crussh/mac.rb +28 -0
  32. data/lib/crussh/negotiator.rb +41 -0
  33. data/lib/crussh/preferred.rb +16 -0
  34. data/lib/crussh/protocol/channel_close.rb +11 -0
  35. data/lib/crussh/protocol/channel_data.rb +12 -0
  36. data/lib/crussh/protocol/channel_eof.rb +11 -0
  37. data/lib/crussh/protocol/channel_extended_data.rb +13 -0
  38. data/lib/crussh/protocol/channel_failure.rb +11 -0
  39. data/lib/crussh/protocol/channel_open.rb +69 -0
  40. data/lib/crussh/protocol/channel_open_confirmation.rb +15 -0
  41. data/lib/crussh/protocol/channel_open_failure.rb +14 -0
  42. data/lib/crussh/protocol/channel_request.rb +146 -0
  43. data/lib/crussh/protocol/channel_success.rb +11 -0
  44. data/lib/crussh/protocol/channel_window_adjust.rb +12 -0
  45. data/lib/crussh/protocol/debug.rb +15 -0
  46. data/lib/crussh/protocol/disconnect.rb +39 -0
  47. data/lib/crussh/protocol/ext_info.rb +48 -0
  48. data/lib/crussh/protocol/global_request.rb +46 -0
  49. data/lib/crussh/protocol/ignore.rb +11 -0
  50. data/lib/crussh/protocol/kex_ecdh_init.rb +11 -0
  51. data/lib/crussh/protocol/kex_ecdh_reply.rb +13 -0
  52. data/lib/crussh/protocol/kex_init.rb +38 -0
  53. data/lib/crussh/protocol/new_keys.rb +9 -0
  54. data/lib/crussh/protocol/ping.rb +11 -0
  55. data/lib/crussh/protocol/pong.rb +11 -0
  56. data/lib/crussh/protocol/request_failure.rb +9 -0
  57. data/lib/crussh/protocol/request_success.rb +11 -0
  58. data/lib/crussh/protocol/service_accept.rb +11 -0
  59. data/lib/crussh/protocol/service_request.rb +11 -0
  60. data/lib/crussh/protocol/unimplemented.rb +11 -0
  61. data/lib/crussh/protocol/userauth_banner.rb +12 -0
  62. data/lib/crussh/protocol/userauth_failure.rb +12 -0
  63. data/lib/crussh/protocol/userauth_pk_ok.rb +12 -0
  64. data/lib/crussh/protocol/userauth_request.rb +52 -0
  65. data/lib/crussh/protocol/userauth_success.rb +9 -0
  66. data/lib/crussh/protocol.rb +135 -0
  67. data/lib/crussh/server/auth_handler.rb +18 -0
  68. data/lib/crussh/server/config.rb +157 -0
  69. data/lib/crussh/server/layers/connection.rb +363 -0
  70. data/lib/crussh/server/layers/transport.rb +49 -0
  71. data/lib/crussh/server/layers/userauth.rb +232 -0
  72. data/lib/crussh/server/request_rule.rb +76 -0
  73. data/lib/crussh/server/session.rb +192 -0
  74. data/lib/crussh/server.rb +214 -0
  75. data/lib/crussh/ssh_id.rb +44 -0
  76. data/lib/crussh/transport/packet_stream.rb +245 -0
  77. data/lib/crussh/transport/reader.rb +98 -0
  78. data/lib/crussh/transport/version_exchange.rb +26 -0
  79. data/lib/crussh/transport/writer.rb +72 -0
  80. data/lib/crussh/version.rb +5 -0
  81. data/lib/crussh.rb +61 -0
  82. data/sig/crussh.rbs +4 -0
  83. metadata +249 -0
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ class Handler
5
+ class LineBuffer
6
+ def initialize(channel, echo: true)
7
+ @channel = channel
8
+ @buffer = +""
9
+ @cursor = 0
10
+ @echo = echo
11
+ end
12
+
13
+ attr_reader :buffer
14
+
15
+ def handle(key)
16
+ case key
17
+ when String
18
+ insert(key)
19
+ when :backspace
20
+ backspace
21
+ when :delete
22
+ delete
23
+ when :arrow_left
24
+ move_left
25
+ when :arrow_right
26
+ move_right
27
+ when :home
28
+ move_to_start
29
+ when :end
30
+ move_to_end
31
+ end
32
+ end
33
+
34
+ def flush
35
+ line = @buffer
36
+ @buffer = +""
37
+ @cursor = 0
38
+ line
39
+ end
40
+
41
+ def clear
42
+ if @echo && @buffer.length.positive?
43
+ move_to_start
44
+ @channel.print("\e[K")
45
+ end
46
+
47
+ @buffer = +""
48
+ @cursor = 0
49
+ end
50
+
51
+ private
52
+
53
+ def cursor_at_start?
54
+ @cursor.zero?
55
+ end
56
+
57
+ def cursor_at_end?
58
+ @cursor == @buffer.length
59
+ end
60
+
61
+ def insert(char)
62
+ if cursor_at_end?
63
+ @buffer << char
64
+ @cursor += 1
65
+ @channel.print(char) if @echo
66
+ return
67
+ end
68
+
69
+ @buffer.insert(@cursor, char)
70
+ @cursor += 1
71
+ redraw_from_cursor if @echo
72
+ end
73
+
74
+ def backspace
75
+ return if cursor_at_start?
76
+
77
+ @cursor -= 1
78
+ @buffer.slice!(@cursor)
79
+
80
+ if @echo
81
+ @channel.print("\b")
82
+ redraw_from_cursor
83
+ end
84
+ end
85
+
86
+ def delete
87
+ return if cursor_at_end?
88
+
89
+ @buffer.slice!(@cursor)
90
+ redraw_from_cursor if @echo
91
+ end
92
+
93
+ def move_left
94
+ return if cursor_at_start?
95
+
96
+ @cursor -= 1
97
+ @channel.print("\e[D") if @echo
98
+ end
99
+
100
+ def move_right
101
+ return if cursor_at_end?
102
+
103
+ @cursor += 1
104
+ @channel.print("\e[C") if @echo
105
+ end
106
+
107
+ def move_to_start
108
+ return if cursor_at_start?
109
+
110
+ @channel.print("\e[#{@cursor}D") if @echo && @cursor > 0
111
+ @cursor = 0
112
+ end
113
+
114
+ def move_to_end
115
+ return if cursor_at_end?
116
+
117
+ distance = @buffer.length - @cursor
118
+ @channel.print("\e[#{distance}C") if @echo && distance > 0
119
+ @cursor = @buffer.length
120
+ end
121
+
122
+ def redraw_from_cursor
123
+ rest = @buffer[@cursor..]
124
+
125
+ @channel.print(rest)
126
+ @channel.print("\e[K")
127
+ @channel.print("\e[#{rest.length}D") if rest.empty?
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/callbacks"
4
+ require "active_support/rescuable"
5
+
6
+ module Crussh
7
+ class Handler
8
+ include ActiveSupport::Callbacks
9
+ include ActiveSupport::Rescuable
10
+
11
+ define_callbacks :handle
12
+
13
+ class << self
14
+ def before(*methods, **options, &block)
15
+ set_callback(:handle, :before, *methods, **options, &block)
16
+ end
17
+
18
+ def after(*methods, **options, &block)
19
+ set_callback(:handle, :after, *methods, **options, &block)
20
+ end
21
+
22
+ def around(*methods, **options, &block)
23
+ set_callback(:handle, :around, *methods, **options, &block)
24
+ end
25
+ end
26
+
27
+ def initialize(channel, session, ...)
28
+ @channel = channel
29
+ @session = session
30
+
31
+ setup(...)
32
+ end
33
+
34
+ def setup(...); end
35
+
36
+ def call
37
+ run_callbacks(:handle) { handle }
38
+ rescue => e
39
+ rescue_with_handler(e) || raise
40
+ end
41
+
42
+ def resize(...); end
43
+
44
+ private
45
+
46
+ attr_reader :session
47
+
48
+ def user = session.user
49
+ def config = session.config
50
+ def pty = channel.pty
51
+ def pty? = channel.pty?
52
+ def env = channel.env
53
+
54
+ def logger
55
+ # TODO: Some sort of logging context
56
+ @logger ||= Logger
57
+ end
58
+
59
+ def puts(...) = channel.puts(...)
60
+ def print(...) = channel.print(...)
61
+ def gets(...) = channel.gets(...)
62
+ def read(...) = channel.read(...)
63
+ def write(...) = channel.write(...)
64
+
65
+ def close = channel.close
66
+ def send_eof = channel.send_eof
67
+ def exit_status(...) = channel.exit_status(...)
68
+ def exit_signal(...) = channel.exit_signal(...)
69
+
70
+ def each_event(...) = channel.each(...)
71
+
72
+ def each_key(&block)
73
+ return enum_for(:each_key) unless block_given?
74
+
75
+ parser = Channel::KeyParser.new
76
+
77
+ each_event do |event|
78
+ case event
79
+ when Channel::Data
80
+ event.each_key(parser: parser, &block)
81
+ when Channel::WindowChange
82
+ resize(event.width, event.height) if respond_to?(:resize, true)
83
+ when Channel::EOF
84
+ yield :eof
85
+ when Channel::Closed
86
+ return
87
+ end
88
+ end
89
+ end
90
+
91
+ def each_line(prompt: "", echo: true)
92
+ return enum_for(:each_line, prompt:, echo:) unless block_given?
93
+
94
+ buffer = LineBuffer.new(channel, echo:)
95
+ print_prompt(prompt)
96
+
97
+ each_key do |key|
98
+ case key
99
+ when :enter
100
+ puts if echo
101
+ line = buffer.flush
102
+ yield line unless line.empty?
103
+ print_prompt(prompt)
104
+ when :interrupt
105
+ puts if echo
106
+ buffer.clear
107
+ print_prompt(prompt)
108
+ when :eof
109
+ puts if echo
110
+ return
111
+ else
112
+ buffer.handle(key)
113
+ end
114
+ end
115
+ end
116
+
117
+ def print_prompt(prompt)
118
+ case prompt
119
+ when String then print(prompt)
120
+ when Proc then print(prompt.call)
121
+ end
122
+ end
123
+
124
+ protected
125
+
126
+ attr_reader(:channel)
127
+ end
128
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ class Heartbeat
5
+ KEEPALIVE_REQUEST = "keepalive@openssh.com"
6
+
7
+ def initialize(session, interval:, max:)
8
+ @session = session
9
+ @interval = interval
10
+ @max = max
11
+ @missed = 0
12
+ @last_activity = Time.now
13
+ @task = nil
14
+ end
15
+
16
+ attr_reader :missed
17
+
18
+ def start(task: Async::Task.current)
19
+ return if @interval.nil?
20
+ return if @task&.running?
21
+
22
+ @task = task.async do
23
+ loop do
24
+ sleep(@interval)
25
+
26
+ if time_since_last_activity >= @interval
27
+ @missed += 1
28
+
29
+ if session_unresponsive?
30
+ @session.disconnect(:connection_lost, "Keepalive timeout")
31
+ break
32
+ end
33
+
34
+ send_keepalive
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def stop
41
+ @task&.stop
42
+ end
43
+
44
+ def record_activity!
45
+ @last_activity = Time.now
46
+ @missed = 0
47
+ end
48
+
49
+ private
50
+
51
+ def send_keepalive
52
+ message = Protocol::GlobalRequest.new(
53
+ request_type: KEEPALIVE_REQUEST,
54
+ )
55
+ @session.write_packet(message)
56
+ rescue IOError, Errno::ECONNRESET, ConnectionClosed
57
+ stop
58
+ end
59
+
60
+ def session_unresponsive?
61
+ @missed > @max
62
+ end
63
+
64
+ def time_since_last_activity
65
+ Time.now - @last_activity
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ module Kex
5
+ class Algorithm
6
+ attr_reader :shared_secret
7
+
8
+ def skip_exchange?
9
+ false
10
+ end
11
+
12
+ def digest(data)
13
+ raise NotImplementedError
14
+ end
15
+
16
+ def client_dh_init
17
+ generate_keypair
18
+
19
+ @public_key
20
+ end
21
+
22
+ def server_dh_reply(client_public)
23
+ generate_keypair
24
+ compute_shared_secret(client_public)
25
+
26
+ @public_key
27
+ end
28
+
29
+ def client_dh_finish(server_public)
30
+ compute_shared_secret(server_public)
31
+ end
32
+
33
+ def compute_exchange_hash(exchange)
34
+ writer = Transport::Writer.new
35
+
36
+ writer
37
+ .string(exchange.client_id)
38
+ .string(exchange.server_id)
39
+ .string(exchange.client_kexinit)
40
+ .string(exchange.server_kexinit)
41
+ .string(exchange.server_host_key)
42
+ .string(exchange.client_public)
43
+ .string(exchange.server_public)
44
+ .mpint(shared_secret)
45
+
46
+ digest(writer.to_s)
47
+ end
48
+
49
+ def derive_keys(session_id:, exchange_hash:, cipher:, mac_c2s:, mac_s2c:, we_are_server:)
50
+ if we_are_server
51
+ {
52
+ iv_send: derive_key("B", cipher.block_size, session_id, exchange_hash),
53
+ iv_recv: derive_key("A", cipher.block_size, session_id, exchange_hash),
54
+ key_send: derive_key("D", cipher.key_length, session_id, exchange_hash),
55
+ key_recv: derive_key("C", cipher.key_length, session_id, exchange_hash),
56
+ mac_send: derive_key("F", mac_s2c&.key_length || 0, session_id, exchange_hash),
57
+ mac_recv: derive_key("E", mac_c2s&.key_length || 0, session_id, exchange_hash),
58
+ }
59
+ else
60
+ {
61
+ iv_send: derive_key("A", cipher.block_size, session_id, exchange_hash),
62
+ iv_recv: derive_key("B", cipher.block_size, session_id, exchange_hash),
63
+ key_send: derive_key("C", cipher.key_length, session_id, exchange_hash),
64
+ key_recv: derive_key("D", cipher.key_length, session_id, exchange_hash),
65
+ mac_send: derive_key("E", mac_c2s&.key_length || 0, session_id, exchange_hash),
66
+ mac_recv: derive_key("F", mac_s2c&.key_length || 0, session_id, exchange_hash),
67
+ }
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def derive_key(letter, needed_length, session_id, exchange_hash)
74
+ return "".b if needed_length == 0
75
+
76
+ writer = Transport::Writer.new
77
+ k_encoded = writer.mpint(shared_secret).to_s
78
+
79
+ key = digest(k_encoded + exchange_hash + letter + session_id)
80
+ key += digest(k_encoded + exchange_hash + key) while key.bytesize < needed_length
81
+
82
+ key[0, needed_length]
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "x25519"
4
+
5
+ module Crussh
6
+ module Kex
7
+ class Curve25519 < Algorithm
8
+ PUBLIC_KEY_SIZE = 32
9
+ PRIVATE_KEY_SIZE = 32
10
+
11
+ def digest(data)
12
+ Digest::SHA256.digest(data)
13
+ end
14
+
15
+ def generate_keypair
16
+ @private_key = X25519::Scalar.generate
17
+ @public_key = @private_key.public_key.to_bytes
18
+ end
19
+
20
+ def compute_shared_secret(their_public_key)
21
+ raise KexError, "Invalid public key size" if their_public_key.bytesize != PUBLIC_KEY_SIZE
22
+
23
+ their_key = X25519::MontgomeryU.new(their_public_key)
24
+ shared = @private_key.diffie_hellman(their_key)
25
+
26
+ @shared_secret = shared.to_bytes.unpack1("H*").to_i(16)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ module Kex
5
+ # TODO: Technically this is just for the server,
6
+ # so maybe we move it to be in the server module scope
7
+ class Exchange
8
+ PING_EXTENSION = "ping@openssh.com"
9
+ SERVER_SIGNATURE_ALGORITHMS = "server-sig-algs"
10
+
11
+ def initialize(session)
12
+ @session = session
13
+ @session_id = session.id
14
+ @received_messages = Set.new
15
+ @extra_messages_before_kexinit = 0
16
+ end
17
+
18
+ attr_reader :algorithms, :session_id
19
+
20
+ def config = @session.config
21
+ def strict_kex? = @session.strict_kex?
22
+ def initial? = @session_id.nil?
23
+
24
+ def initial(client_version:)
25
+ @client_version = client_version
26
+ @client_kexinit_payload = read_initial_kexinit
27
+
28
+ client_kexinit = Protocol::KexInit.parse(@client_kexinit_payload)
29
+
30
+ validate_strictness!(client_kexinit)
31
+
32
+ perform_full_key_exchange(client_kexinit)
33
+
34
+ if client_supports_ext_info?(client_kexinit)
35
+ send_ext_info
36
+ end
37
+ end
38
+
39
+ def start_rekey
40
+ server_kexinit = Protocol::KexInit.from_preferred(config.preferred)
41
+ @server_kexinit_payload = server_kexinit.serialize
42
+ @session.write_raw_packet(server_kexinit)
43
+
44
+ @client_kexinit_payload = @session.read_raw_packet
45
+ client_kexinit = Protocol::KexInit.parse(@client_kexinit_payload)
46
+
47
+ negotiate_and_exchange(client_kexinit, server_kexinit)
48
+ end
49
+
50
+ def rekey(client_kexinit_payload:)
51
+ @client_kexinit_payload = client_kexinit_payload
52
+ client_kexinit = Protocol::KexInit.parse(@client_kexinit_payload)
53
+
54
+ perform_full_key_exchange(client_kexinit)
55
+ end
56
+
57
+ private
58
+
59
+ def perform_full_key_exchange(client_kexinit)
60
+ server_kexinit = Protocol::KexInit.from_preferred(config.preferred)
61
+ @server_kexinit_payload = server_kexinit.serialize
62
+ @session.write_raw_packet(server_kexinit)
63
+
64
+ negotiate_and_exchange(client_kexinit, server_kexinit)
65
+ end
66
+
67
+ def validate_strictness!(client_kexinit)
68
+ @session.strict_kex = client_supports_strict?(client_kexinit)
69
+
70
+ if strict_kex? && @extra_messages_before_kexinit.positive?
71
+ raise ProtocolError, "Strict KEX: KEXINIT was not the first message"
72
+ end
73
+ end
74
+
75
+ def negotiate_and_exchange(client_kexinit, server_kexinit)
76
+ @algorithms = Negotiator.new(client_kexinit, server_kexinit).negotiate
77
+
78
+ perform_dh_exchange
79
+ derive_and_enable_keys
80
+ end
81
+
82
+ def perform_dh_exchange
83
+ packet = read_kex_packet(Protocol::KEX_ECDH_INIT)
84
+ kex_dh_init = Protocol::KexEcdhInit.parse(packet)
85
+ client_public = kex_dh_init.public_key
86
+
87
+ @kex_algorithm = Kex.from_name(@algorithms.kex)
88
+ server_public = @kex_algorithm.server_dh_reply(client_public)
89
+
90
+ parameters = Parameters.new(
91
+ client_id: @client_version.to_s,
92
+ server_id: config.server_id.to_s,
93
+ client_kexinit: @client_kexinit_payload,
94
+ server_kexinit: @server_kexinit_payload,
95
+ server_host_key: host_key.public_key_blob,
96
+ client_public: client_public,
97
+ server_public: server_public,
98
+ shared_secret: @kex_algorithm.shared_secret,
99
+ )
100
+
101
+ @exchange_hash = @kex_algorithm.compute_exchange_hash(parameters)
102
+ signature = host_key.sign(@exchange_hash)
103
+
104
+ kex_ecdh_reply = Protocol::KexEcdhReply.new(
105
+ public_host_key: host_key.public_key_blob,
106
+ public_key: server_public,
107
+ signature: signature,
108
+ )
109
+
110
+ @session.write_raw_packet(kex_ecdh_reply)
111
+ @session.write_raw_packet(Protocol::NewKeys.new)
112
+
113
+ packet = read_kex_packet(Protocol::NEWKEYS)
114
+ Protocol::NewKeys.parse(packet)
115
+
116
+ @session_id ||= @exchange_hash
117
+ end
118
+
119
+ def derive_and_enable_keys
120
+ cipher = Cipher.from_name(@algorithms.cipher_server_to_client)
121
+
122
+ keys = @kex_algorithm.derive_keys(
123
+ session_id: @session_id,
124
+ exchange_hash: @exchange_hash,
125
+ cipher: cipher,
126
+ mac_c2s: nil,
127
+ mac_s2c: nil,
128
+ we_are_server: true,
129
+ )
130
+
131
+ opening_key = cipher.make_opening_key(key: keys[:key_recv])
132
+ sealing_key = cipher.make_sealing_key(key: keys[:key_send])
133
+
134
+ @session.enable_encryption(opening_key, sealing_key)
135
+ @session.reset_sequence if strict_kex?
136
+ @session.algorithms = @algorithms
137
+
138
+ Logger.info(self, "Keys exchanged", cipher: @algorithms.cipher_server_to_client)
139
+ end
140
+
141
+ def send_ext_info
142
+ signature_algorithms = config.preferred.host_key.join(",")
143
+
144
+ extensions = {
145
+ SERVER_SIGNATURE_ALGORITHMS => signature_algorithms,
146
+ PING_EXTENSION => "0",
147
+ }
148
+
149
+ ext_info = Protocol::ExtInfo.new(extensions: extensions)
150
+ @session.write_packet(ext_info)
151
+ end
152
+
153
+ def read_kex_packet(expected_type)
154
+ validate_sequence_hasnt_wrapped! if strict_kex? && initial?
155
+
156
+ packet = @session.read_raw_packet
157
+ message_type = packet.getbyte(0)
158
+
159
+ if strict_kex? && initial?
160
+ unless kex_message?(message_type)
161
+ raise ProtocolError, "Strict KEX: unexpected message type #{message_type} during initial KEX"
162
+ end
163
+
164
+ if @received_messages.include?(message_type)
165
+ raise ProtocolError, "Strict KEX: duplicate message type #{message_type}"
166
+ end
167
+
168
+ @received_messages.add(message_type)
169
+ else
170
+ while message_type == Protocol::IGNORE || message_type == Protocol::DEBUG
171
+ packet = @session.read_raw_packet
172
+ message_type = packet.getbyte(0)
173
+ end
174
+ end
175
+
176
+ unless message_type == expected_type
177
+ raise ProtocolError, "Unexpected message type #{message_type}, expected #{expected_type}"
178
+ end
179
+
180
+ packet
181
+ end
182
+
183
+ def read_initial_kexinit
184
+ packet = @session.read_raw_packet
185
+ message_type = packet.getbyte(0)
186
+
187
+ while message_type == Protocol::IGNORE || message_type == Protocol::DEBUG
188
+ @extra_messages_before_kexinit += 1
189
+ packet = @session.read_raw_packet
190
+ message_type = packet.getbyte(0)
191
+ end
192
+
193
+ unless message_type == Protocol::KEXINIT
194
+ raise ProtocolError, "Expected KEXINIT, got message type #{message_type}"
195
+ end
196
+
197
+ @received_messages.add(Protocol::KEXINIT)
198
+
199
+ packet
200
+ end
201
+
202
+ def validate_sequence_hasnt_wrapped!
203
+ return unless @session.sequence_wrapped?
204
+
205
+ raise ProtocolError, "Strict KEX: sequence number wrapped during initial KEX"
206
+ end
207
+
208
+ def client_supports_strict?(client_kexinit)
209
+ client_kexinit.kex_algorithms.include?(STRICT_CLIENT)
210
+ end
211
+
212
+ def client_supports_ext_info?(client_kexinit)
213
+ client_kexinit.kex_algorithms.include?(EXT_INFO_CLIENT)
214
+ end
215
+
216
+ def kex_message?(type)
217
+ case type
218
+ when Protocol::KEXINIT, Protocol::NEWKEYS,
219
+ Protocol::KEX_ECDH_INIT, Protocol::KEX_ECDH_REPLY
220
+ # TODO: Add other KEX message types when implementing those algorithms:
221
+ # - SSH_MSG_KEXDH_INIT (30), SSH_MSG_KEXDH_REPLY (31) for DH
222
+ # - SSH_MSG_KEX_DH_GEX_* for DH group exchange
223
+ true
224
+ else
225
+ false
226
+ end
227
+ end
228
+
229
+ def host_key
230
+ @host_key ||= config.host_keys.find { |key| key.algorithm == @algorithms.host_key }
231
+ end
232
+ end
233
+ end
234
+ end