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,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ class Server
5
+ module Layers
6
+ class Transport
7
+ def initialize(session)
8
+ @session = session
9
+ @client_version = nil
10
+ @algorithms = nil
11
+ @session_id = nil
12
+ @client_kexinit_payload = nil
13
+ @server_kexinit_payload = nil
14
+ end
15
+
16
+ attr_reader :client_version, :algorithms, :session_id
17
+
18
+ def run
19
+ version_exchange
20
+ key_exchange
21
+ end
22
+
23
+ private
24
+
25
+ def config = @session.config
26
+ def socket = @session.socket
27
+
28
+ def version_exchange
29
+ exchange = ::Crussh::Transport::VersionExchange.new(socket, server_id: config.server_id)
30
+
31
+ @client_version = exchange.exchange
32
+
33
+ Logger.info(
34
+ self,
35
+ "Version exchange complete",
36
+ client_version: @client_version.software_version,
37
+ comments: @client_version.comments,
38
+ )
39
+ end
40
+
41
+ def key_exchange
42
+ kex = Kex::Exchange.new(@session)
43
+ kex.initial(client_version: @client_version)
44
+ @session_id = kex.session_id
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ class Server
5
+ module Layers
6
+ class Userauth
7
+ def initialize(session)
8
+ @session = session
9
+ @authenticated_user = nil
10
+ @attempts = 0
11
+ @first_attempt = true
12
+ end
13
+
14
+ attr_reader :authenticated_user
15
+
16
+ def run(task: Async::Task.current)
17
+ timeout = config.auth_timeout || config.connection_timeout
18
+
19
+ task.with_timeout(timeout) do
20
+ service_request
21
+ send_banner
22
+ authenticate
23
+ end
24
+ rescue Async::TimeoutError
25
+ Logger.warn(self, "Authentication timeout")
26
+ end
27
+
28
+ private
29
+
30
+ def config = @session.config
31
+ def server = @session.server
32
+ def session_id = @session.id
33
+
34
+ def supported_methods
35
+ server.auth_methods.map(&:to_s)
36
+ end
37
+
38
+ def service_request
39
+ packet = @session.read_packet
40
+ request = Protocol::ServiceRequest.parse(packet)
41
+
42
+ Logger.debug(self, "Service request", service: request.service_name)
43
+
44
+ unless request.service_name == "ssh-userauth"
45
+ raise ProtocolError, "Unknown service: #{request.service_name}"
46
+ end
47
+
48
+ accept = Protocol::ServiceAccept.new(service_name: "ssh-userauth")
49
+ @session.write_packet(accept)
50
+
51
+ Logger.debug(self, "Service accepted", service: "ssh-userauth")
52
+ end
53
+
54
+ def send_banner
55
+ banner = server.banner
56
+
57
+ return if banner.nil?
58
+
59
+ packet = Protocol::UserauthBanner.new(message: banner)
60
+ @session.write_packet(packet)
61
+ Logger.debug(self, "Banner sent")
62
+ end
63
+
64
+ def authenticate
65
+ loop do
66
+ packet = @session.read_packet
67
+ message_type = packet.getbyte(0)
68
+
69
+ case message_type
70
+ when Protocol::USERAUTH_REQUEST
71
+ handle_auth_request(packet)&.then { return }
72
+ when Protocol::DISCONNECT
73
+ Logger.debug(self, "Client disconnected during auth")
74
+ @session.close
75
+ return
76
+ else
77
+ Logger.warn(self, "Unknown message type during authentication", message_type:)
78
+ unimplemented = Protocol::Unimplemented.new(sequence_number: @session.last_read_sequence)
79
+ @session.write_packet(unimplemented)
80
+ end
81
+ end
82
+ end
83
+
84
+ def handle_auth_request(packet)
85
+ request = Protocol::UserauthRequest.parse(packet)
86
+
87
+ Logger.debug(
88
+ self,
89
+ "Auth request",
90
+ user: request.username,
91
+ service: request.service_name,
92
+ method: request.method_name,
93
+ )
94
+
95
+ result = dispatch_auth(request)
96
+
97
+ case result
98
+ when :success
99
+ handle_successful_auth(request)
100
+ true
101
+ when :pk_ok, :partial
102
+ nil
103
+ when :failure
104
+ handle_failed_auth(request)
105
+ nil
106
+ end
107
+ end
108
+
109
+ def dispatch_auth(request)
110
+ method = request.method_name.to_sym
111
+
112
+ result = case method
113
+ when :none
114
+ server.handle_auth(:none, request.username)
115
+ when :password
116
+ server.handle_auth(:password, request.username, request.password)
117
+ when :publickey
118
+ handle_publickey(request)
119
+ when :keyboard_interactive
120
+ Auth.reject
121
+ else
122
+ Logger.warn(self, "Unknown auth method", method: request.method_name)
123
+ Auth.reject
124
+ end
125
+
126
+ case result
127
+ when lambda(&:success?)
128
+ :success
129
+ when lambda(&:partial?)
130
+ handle_partial_success(request, result)
131
+ :partial
132
+ else
133
+ :failure
134
+ end
135
+ end
136
+
137
+ def handle_publickey(request)
138
+ pk_data = request.public_key_data
139
+ public_key = Keys.parse_public_blob(pk_data.key_blob)
140
+
141
+ unless pk_data.has_signature?
142
+ acceptable = server.handle_auth(:publickey, request.username, public_key)
143
+
144
+ if acceptable
145
+ pk_ok = Protocol::UserauthPkOk.new(
146
+ algorithm: pk_data.algorithm,
147
+ key_blob: pk_data.key_blob,
148
+ )
149
+ @session.write_packet(pk_ok)
150
+
151
+ Logger.debug(self, "PK_OK sent", algorithm: pk_data.algorithm)
152
+
153
+ :pk_ok
154
+ else
155
+ :failure
156
+ end
157
+ end
158
+
159
+ signed = build_signed_data(request, pk_data)
160
+
161
+ if public_key.verify(signed, pk_data.signature)
162
+ :success
163
+ else
164
+ :failure
165
+ end
166
+ end
167
+
168
+ def handle_successful_auth(request)
169
+ @authenticated_user = request.username
170
+ @session.write_packet(Protocol::UserauthSuccess.new)
171
+ server.auth_succeeded(request.username) if server.respond_to?(:auth_succeeded)
172
+
173
+ @session.enable_compression
174
+
175
+ Logger.info(
176
+ self,
177
+ "Authentication successful",
178
+ user: request.username,
179
+ method: request.method_name,
180
+ )
181
+ end
182
+
183
+ def handle_partial_success(request, result)
184
+ methods = result.continue_with.map(&:to_s)
185
+ packet = Protocol::UserauthFailure.new(
186
+ authentications: methods,
187
+ partial_success: true,
188
+ )
189
+ @session.write_packet(packet)
190
+ end
191
+
192
+ def handle_failed_auth(request)
193
+ @attempts += 1
194
+
195
+ rejection_time = if initial?(request)
196
+ config.auth_rejection_time_initial
197
+ else
198
+ config.auth_rejection_time
199
+ end
200
+
201
+ @first_attempt = false
202
+
203
+ sleep(rejection_time) if rejection_time&.positive?
204
+
205
+ packet = Protocol::UserauthFailure.new(authentications: supported_methods)
206
+ @session.write_packet(packet)
207
+
208
+ return if @attempts < config.max_auth_attempts
209
+
210
+ @session.disconnect(:no_more_auth_methods_available, "Too many authentication failures")
211
+ end
212
+
213
+ def build_signed_data(request, pk_data)
214
+ writer = Crussh::Transport::Writer.new
215
+ writer.string(session_id)
216
+ writer.byte(Protocol::USERAUTH_REQUEST)
217
+ writer.string(request.username)
218
+ writer.string(request.service_name)
219
+ writer.string("publickey")
220
+ writer.boolean(true)
221
+ writer.string(pk_data.algorithm)
222
+ writer.string(pk_data.key_blob)
223
+ writer.to_s
224
+ end
225
+
226
+ def initial?(request)
227
+ @first_attempt && request.none?
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ class Server
5
+ class RequestRule
6
+ class << self
7
+ def accept(only: nil, except: nil, if: nil, unless: nil, &block)
8
+ new(
9
+ allow: true,
10
+ only: only,
11
+ except: except,
12
+ if: binding.local_variable_get(:if),
13
+ unless: binding.local_variable_get(:unless),
14
+ &block
15
+ )
16
+ end
17
+
18
+ def reject
19
+ new(allow: false, only: nil, except: nil, if: nil, unless: nil, &block)
20
+ end
21
+ end
22
+
23
+ def initialize(allow: true, only: nil, except: nil, if: nil, unless: nil, &block)
24
+ @allow = allow
25
+ @only = only
26
+ @except = except
27
+ @if = binding.local_variable_get(:if)
28
+ @unless = binding.local_variable_get(:unless)
29
+ @block = block
30
+ end
31
+
32
+ attr_reader :allow, :only, :except, :if, :unless, :block
33
+
34
+ def allowed?(channel, **params)
35
+ return false unless allow
36
+
37
+ if only
38
+ value = params[:name] || params[:term]
39
+ return false unless matches_patterns?(only, value)
40
+ end
41
+
42
+ if except
43
+ value = params[:name] || params[:term]
44
+ return false if matches_patterns?(except, value)
45
+ end
46
+
47
+ return false if self.if && !self.if.call(channel, **params)
48
+ return false if self.unless&.call(channel, **params)
49
+ return block.call(channel, **params) if block
50
+
51
+ true
52
+ end
53
+
54
+ private
55
+
56
+ def matches_patterns?(patterns, value)
57
+ return true if value.nil?
58
+
59
+ Array(patterns).any? do |pattern|
60
+ case pattern
61
+ when Regexp
62
+ pattern.match?(value)
63
+ when String
64
+ if pattern.include?("*")
65
+ File.fnmatch?(pattern, value)
66
+ else
67
+ pattern == value
68
+ end
69
+ else
70
+ pattern == value
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crussh
4
+ class Server
5
+ class Session
6
+ def initialize(socket, server:)
7
+ @socket = socket
8
+ @server = server
9
+
10
+ @packet_stream = Transport::PacketStream.new(socket, max_packet_size: config.max_packet_size)
11
+
12
+ @bytes_read = 0
13
+ @bytes_written = 0
14
+ @last_kex_time = Time.now
15
+ @algorithms = nil
16
+
17
+ @heartbeat = nil
18
+
19
+ @strict_kex = false
20
+ end
21
+
22
+ attr_reader :client_version, :socket, :server, :user, :id
23
+ attr_accessor :algorithms
24
+ attr_writer :strict_kex
25
+
26
+ def config = @server.config
27
+ def strict_kex? = @strict_kex
28
+
29
+ def start
30
+ transport = run_layer(Layers::Transport)
31
+ @id = transport.session_id
32
+
33
+ userauth = run_layer(Layers::Userauth)
34
+ @user = userauth.authenticated_user
35
+
36
+ @server.gatekeeper.authenticate!
37
+
38
+ start_heartbeat
39
+
40
+ run_layer(Layers::Connection)
41
+
42
+ Logger.info(self, "Session established", user: @user)
43
+ rescue NegotiationError => e
44
+ Logger.error(self, "Negotation Error", e)
45
+ disconnect(:key_exchange_failed, e.message)
46
+ rescue ProtocolError => e
47
+ Logger.error(self, "Protocol Error", e)
48
+ disconnect(:protocol_error, e.message)
49
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET, ConnectionClosed => e
50
+ Logger.debug(self, "Connection closed", reason: e.class.name)
51
+ rescue StandardError => e
52
+ Logger.error(self, "Internal Server Error", e)
53
+ disconnect(:by_application, "Internal error")
54
+ ensure
55
+ stop_heartbeat
56
+ close
57
+ end
58
+
59
+ def disconnect(reason, description = "")
60
+ message = Protocol::Disconnect.build(reason, description)
61
+ write_raw_packet(message)
62
+ stop_heartbeat
63
+ close
64
+ end
65
+
66
+ def close
67
+ return if socket.closed?
68
+
69
+ @socket.close
70
+ rescue StandardError => e
71
+ Logger.error(self, "Error", e)
72
+ end
73
+
74
+ def read_packet
75
+ start_rekey if rekey?
76
+
77
+ @heartbeat&.record_activity!
78
+
79
+ loop do
80
+ packet = read_raw_packet
81
+ message_type = packet.getbyte(0)
82
+
83
+ case message_type
84
+ when Protocol::IGNORE, Protocol::EXT_INFO
85
+ next
86
+ when Protocol::DEBUG
87
+ message = Protocol::Debug.parse(packet)
88
+ Logger.debug(self, "Client debug", message: message.message) if message.always_display?
89
+ next
90
+ when Protocol::PING
91
+ pong(packet)
92
+ next
93
+ when Protocol::KEXINIT
94
+ rekey(packet)
95
+ next
96
+ else
97
+ @bytes_read += packet.bytesize
98
+ return packet
99
+ end
100
+ end
101
+ end
102
+
103
+ def write_packet(message)
104
+ start_rekey if rekey?
105
+
106
+ data = message.serialize
107
+ @bytes_written += data.bytesize
108
+ @packet_stream.write(data)
109
+ end
110
+
111
+ def read_raw_packet = @packet_stream.read
112
+ def write_raw_packet(message) = @packet_stream.write(message.serialize)
113
+
114
+ def enable_encryption(...) = @packet_stream.enable_encryption(...)
115
+
116
+ def enable_compression
117
+ return if @algorithms.nil?
118
+
119
+ c2s = @algorithms.compression_client_to_server
120
+ s2c = @algorithms.compression_server_to_client
121
+
122
+ return if c2s == Compression::NONE && s2c == Compression::NONE
123
+
124
+ read_compressor = Compression.from_name(c2s)
125
+ write_compressor = Compression.from_name(s2c)
126
+
127
+ @packet_stream.enable_compression(read_compressor, write_compressor)
128
+ Logger.info(self, "Compression enabled", send: s2c, recv: c2s)
129
+ end
130
+
131
+ def last_read_sequence = @packet_stream.last_read_sequence
132
+ def reset_sequence = @packet_stream.reset_sequence
133
+ def sequence_wrapped? = @packet_stream.sequence_wrapped?
134
+
135
+ private
136
+
137
+ def rekey?
138
+ limits = config.limits
139
+
140
+ limits.over?(read: @bytes_read, written: @bytes_written, time: @last_kex_time)
141
+ end
142
+
143
+ def start_rekey
144
+ Logger.info(self, "Initiating rekey to client")
145
+
146
+ kex = Kex::Exchange.new(self)
147
+ kex.start_rekey
148
+ reset_rekey_tracking
149
+ end
150
+
151
+ def pong(packet)
152
+ ping = Protocol::Ping.parse(packet)
153
+ pong = Protocol::Pong.new(data: ping.data)
154
+
155
+ write_packet(pong)
156
+ end
157
+
158
+ def rekey(client_kexinit_payload)
159
+ Logger.info(self, "Client initiated rekey")
160
+
161
+ kex = Kex::Exchange.new(self)
162
+ kex.rekey(client_kexinit_payload: client_kexinit_payload)
163
+
164
+ Logger.info(self, "Rekey complete")
165
+ end
166
+
167
+ def reset_rekey_tracking
168
+ @bytes_read = 0
169
+ @bytes_written = 0
170
+ @last_kex_time = Time.now
171
+ end
172
+
173
+ def start_heartbeat
174
+ return if config.keepalive_interval.nil?
175
+
176
+ @heartbeat = Heartbeat.new(self, interval: config.keepalive_interval, max: config.keepalive_max)
177
+
178
+ @heartbeat.start
179
+ end
180
+
181
+ def stop_heartbeat
182
+ @heartbeat&.stop
183
+ end
184
+
185
+ def run_layer(layer)
186
+ instance = layer.new(self)
187
+ instance.run
188
+ instance
189
+ end
190
+ end
191
+ end
192
+ end