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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +371 -0
- data/ext/poly1305/Cargo.toml +13 -0
- data/ext/poly1305/extconf.rb +6 -0
- data/ext/poly1305/src/lib.rs +75 -0
- data/lib/crussh/auth.rb +46 -0
- data/lib/crussh/channel/key_parser.rb +125 -0
- data/lib/crussh/channel.rb +381 -0
- data/lib/crussh/cipher/algorithm.rb +31 -0
- data/lib/crussh/cipher/chacha20poly1305.rb +98 -0
- data/lib/crussh/cipher.rb +25 -0
- data/lib/crussh/compression.rb +42 -0
- data/lib/crussh/gatekeeper.rb +50 -0
- data/lib/crussh/handler/line_buffer.rb +131 -0
- data/lib/crussh/handler.rb +128 -0
- data/lib/crussh/heartbeat.rb +68 -0
- data/lib/crussh/kex/algorithm.rb +86 -0
- data/lib/crussh/kex/curve25519.rb +30 -0
- data/lib/crussh/kex/exchange.rb +234 -0
- data/lib/crussh/kex.rb +42 -0
- data/lib/crussh/keys/key_pair.rb +61 -0
- data/lib/crussh/keys/public_key.rb +35 -0
- data/lib/crussh/keys.rb +70 -0
- data/lib/crussh/limits.rb +45 -0
- data/lib/crussh/logger.rb +95 -0
- data/lib/crussh/mac/algorithm.rb +23 -0
- data/lib/crussh/mac/crypto.rb +60 -0
- data/lib/crussh/mac/none.rb +9 -0
- data/lib/crussh/mac.rb +28 -0
- data/lib/crussh/negotiator.rb +41 -0
- data/lib/crussh/preferred.rb +16 -0
- data/lib/crussh/protocol/channel_close.rb +11 -0
- data/lib/crussh/protocol/channel_data.rb +12 -0
- data/lib/crussh/protocol/channel_eof.rb +11 -0
- data/lib/crussh/protocol/channel_extended_data.rb +13 -0
- data/lib/crussh/protocol/channel_failure.rb +11 -0
- data/lib/crussh/protocol/channel_open.rb +69 -0
- data/lib/crussh/protocol/channel_open_confirmation.rb +15 -0
- data/lib/crussh/protocol/channel_open_failure.rb +14 -0
- data/lib/crussh/protocol/channel_request.rb +146 -0
- data/lib/crussh/protocol/channel_success.rb +11 -0
- data/lib/crussh/protocol/channel_window_adjust.rb +12 -0
- data/lib/crussh/protocol/debug.rb +15 -0
- data/lib/crussh/protocol/disconnect.rb +39 -0
- data/lib/crussh/protocol/ext_info.rb +48 -0
- data/lib/crussh/protocol/global_request.rb +46 -0
- data/lib/crussh/protocol/ignore.rb +11 -0
- data/lib/crussh/protocol/kex_ecdh_init.rb +11 -0
- data/lib/crussh/protocol/kex_ecdh_reply.rb +13 -0
- data/lib/crussh/protocol/kex_init.rb +38 -0
- data/lib/crussh/protocol/new_keys.rb +9 -0
- data/lib/crussh/protocol/ping.rb +11 -0
- data/lib/crussh/protocol/pong.rb +11 -0
- data/lib/crussh/protocol/request_failure.rb +9 -0
- data/lib/crussh/protocol/request_success.rb +11 -0
- data/lib/crussh/protocol/service_accept.rb +11 -0
- data/lib/crussh/protocol/service_request.rb +11 -0
- data/lib/crussh/protocol/unimplemented.rb +11 -0
- data/lib/crussh/protocol/userauth_banner.rb +12 -0
- data/lib/crussh/protocol/userauth_failure.rb +12 -0
- data/lib/crussh/protocol/userauth_pk_ok.rb +12 -0
- data/lib/crussh/protocol/userauth_request.rb +52 -0
- data/lib/crussh/protocol/userauth_success.rb +9 -0
- data/lib/crussh/protocol.rb +135 -0
- data/lib/crussh/server/auth_handler.rb +18 -0
- data/lib/crussh/server/config.rb +157 -0
- data/lib/crussh/server/layers/connection.rb +363 -0
- data/lib/crussh/server/layers/transport.rb +49 -0
- data/lib/crussh/server/layers/userauth.rb +232 -0
- data/lib/crussh/server/request_rule.rb +76 -0
- data/lib/crussh/server/session.rb +192 -0
- data/lib/crussh/server.rb +214 -0
- data/lib/crussh/ssh_id.rb +44 -0
- data/lib/crussh/transport/packet_stream.rb +245 -0
- data/lib/crussh/transport/reader.rb +98 -0
- data/lib/crussh/transport/version_exchange.rb +26 -0
- data/lib/crussh/transport/writer.rb +72 -0
- data/lib/crussh/version.rb +5 -0
- data/lib/crussh.rb +61 -0
- data/sig/crussh.rbs +4 -0
- metadata +249 -0
data/lib/crussh/kex.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Kex
|
|
5
|
+
STRICT_SERVER = "kex-strict-s-v00@openssh.com"
|
|
6
|
+
STRICT_CLIENT = "kex-strict-c-v00@openssh.com"
|
|
7
|
+
|
|
8
|
+
EXT_INFO_SERVER = "ext-info-s"
|
|
9
|
+
EXT_INFO_CLIENT = "ext-info-c"
|
|
10
|
+
|
|
11
|
+
CURVE25519_SHA256 = "curve25519-sha256"
|
|
12
|
+
CURVE25519_SHA256_LIBSSH = "curve25519-sha256@libssh.org"
|
|
13
|
+
|
|
14
|
+
DEFAULT = [CURVE25519_SHA256, CURVE25519_SHA256_LIBSSH, STRICT_SERVER, EXT_INFO_SERVER]
|
|
15
|
+
|
|
16
|
+
REGISTRY = {
|
|
17
|
+
CURVE25519_SHA256 => Curve25519,
|
|
18
|
+
CURVE25519_SHA256_LIBSSH => Curve25519,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def from_name(name)
|
|
23
|
+
algorithm_class = REGISTRY[name]
|
|
24
|
+
|
|
25
|
+
raise UnknownAlgorithm, "Unknown KEX algorithm: #{name}" if algorithm_class.nil?
|
|
26
|
+
|
|
27
|
+
algorithm_class.new
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Parameters = Data.define(
|
|
32
|
+
:client_id,
|
|
33
|
+
:server_id,
|
|
34
|
+
:client_kexinit,
|
|
35
|
+
:server_kexinit,
|
|
36
|
+
:server_host_key,
|
|
37
|
+
:client_public,
|
|
38
|
+
:server_public,
|
|
39
|
+
:shared_secret,
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Keys
|
|
5
|
+
class KeyPair
|
|
6
|
+
def initialize(private_key, signature_algorithm: nil)
|
|
7
|
+
@private_key = private_key
|
|
8
|
+
@signature_algorithm = signature_algorithm || default_signature_algorithm
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :signature_algorithm
|
|
12
|
+
|
|
13
|
+
def algorithm
|
|
14
|
+
@private_key.public_key.algo
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def public_key_blob
|
|
18
|
+
@private_key.public_key.rfc4253
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def sign(data)
|
|
22
|
+
@private_key.sign(data, algo: @signature_algorithm)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def verify(data, signature)
|
|
26
|
+
@private_key.public_key.verify(data, signature)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def public_key
|
|
30
|
+
@public_key ||= PublicKey.new(@private_key.public_key)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fingerprint
|
|
34
|
+
public_key.fingerprint
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_authorized_key(comment: nil)
|
|
38
|
+
public_key.to_authorized_key(comment:)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_openssh(passphrase: nil, comment: "")
|
|
42
|
+
@private_key.openssh(comment: comment)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def default_signature_algorithm
|
|
48
|
+
case @private_key
|
|
49
|
+
when SSHData::PrivateKey::ED25519
|
|
50
|
+
ED25519
|
|
51
|
+
when SSHData::PrivateKey::RSA
|
|
52
|
+
RSA_SHA512
|
|
53
|
+
when SSHData::PrivateKey::ECDSA
|
|
54
|
+
@private_key.public_key.algo
|
|
55
|
+
else
|
|
56
|
+
@private_key.public_key.algo
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Keys
|
|
5
|
+
class PublicKey
|
|
6
|
+
def initialize(public_key)
|
|
7
|
+
@public_key = public_key
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def algorithm
|
|
11
|
+
@public_key.algo
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def encode
|
|
15
|
+
@public_key.rfc4253
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def verify(data, signature)
|
|
19
|
+
@public_key.verify(data, signature)
|
|
20
|
+
rescue SSHData::VerifyError
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fingerprint
|
|
25
|
+
@public_key.fingerprint
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_authorized_key(comment: nil)
|
|
29
|
+
parts = [@public_key.algo, Base64.strict_encode64(@public_key.encode)]
|
|
30
|
+
parts << comment if comment
|
|
31
|
+
parts.join(" ")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/crussh/keys.rb
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ed25519"
|
|
4
|
+
require "ssh_data"
|
|
5
|
+
|
|
6
|
+
module Crussh
|
|
7
|
+
module Keys
|
|
8
|
+
ED25519 = "ssh-ed25519"
|
|
9
|
+
RSA_SHA256 = "rsa-sha2-256"
|
|
10
|
+
RSA_SHA512 = "rsa-sha2-512"
|
|
11
|
+
ECDSA_P256 = "ecdsa-sha2-nistp256"
|
|
12
|
+
ECDSA_P384 = "ecdsa-sha2-nistp384"
|
|
13
|
+
ECDSA_P521 = "ecdsa-sha2-nistp521"
|
|
14
|
+
|
|
15
|
+
DEFAULT = [
|
|
16
|
+
ED25519,
|
|
17
|
+
RSA_SHA512,
|
|
18
|
+
RSA_SHA256,
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def generate(algorithm = ED25519)
|
|
23
|
+
private_key = case algorithm
|
|
24
|
+
when ED25519
|
|
25
|
+
SSHData::PrivateKey::ED25519.generate
|
|
26
|
+
when RSA_SHA256, RSA_SHA512
|
|
27
|
+
SSHData::PrivateKey::RSA.generate
|
|
28
|
+
when ECDSA_P256
|
|
29
|
+
SSHData::PrivateKey::ECDSA.generate("nistp256")
|
|
30
|
+
when ECDSA_P384
|
|
31
|
+
SSHData::PrivateKey::ECDSA.generate("nistp384")
|
|
32
|
+
when ECDSA_P521
|
|
33
|
+
SSHData::PrivateKey::ECDSA.generate("nistp521")
|
|
34
|
+
else
|
|
35
|
+
raise KeyError, "Unknown algorithm: #{algorithm}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
KeyPair.new(private_key, signature_algorithm: algorithm)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def from_openssh(data, passphrase: nil)
|
|
42
|
+
private_key = SSHData::PrivateKey.parse_openssh(data, passphrase:)
|
|
43
|
+
KeyPair.new(private_key)
|
|
44
|
+
rescue SSHData::DecryptError
|
|
45
|
+
raise KeyError, "Invalid passphrase or corrupted key"
|
|
46
|
+
rescue SSHData::DecodeError => e
|
|
47
|
+
raise KeyError, "Failed to parse key: #{e.message}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def from_file(path, passphrase: nil)
|
|
51
|
+
from_openssh(File.read(path), passphrase:)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse_public_blob(data)
|
|
55
|
+
public_key = SSHData::PublicKey.parse(data)
|
|
56
|
+
|
|
57
|
+
PublicKey.new(public_key)
|
|
58
|
+
rescue SSHData::DecodeError => e
|
|
59
|
+
raise KeyError, "Failed to parse public key: #{e.message}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def parse_authorized_key(data)
|
|
63
|
+
public_key = SSHData::PublicKey.parse_openssh(data)
|
|
64
|
+
PublicKey.new(public_key)
|
|
65
|
+
rescue SSHData::DecodeError => e
|
|
66
|
+
raise KeyError, "Failed to parse public key: #{e.message}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
# Thresholds for when to perform key re-exchange.
|
|
5
|
+
# Rekeying is important for long-lived connections to limit
|
|
6
|
+
# the amount of data encrypted under a single key.
|
|
7
|
+
class Limits
|
|
8
|
+
ONE_GB = 1 << 30
|
|
9
|
+
ONE_HOUR = 3600
|
|
10
|
+
|
|
11
|
+
attr_accessor :rekey_write_limit,
|
|
12
|
+
:rekey_read_limit,
|
|
13
|
+
:rekey_time_limit
|
|
14
|
+
|
|
15
|
+
# defaults from
|
|
16
|
+
# https://datatracker.ietf.org/doc/html/rfc4253#section-9
|
|
17
|
+
def initialize(
|
|
18
|
+
rekey_write_limit: ONE_GB,
|
|
19
|
+
rekey_read_limit: ONE_GB,
|
|
20
|
+
rekey_time_limit: ONE_HOUR
|
|
21
|
+
)
|
|
22
|
+
@rekey_write_limit = rekey_write_limit
|
|
23
|
+
@rekey_read_limit = rekey_read_limit
|
|
24
|
+
@rekey_time_limit = rekey_time_limit
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def over?(read:, written:, time:)
|
|
28
|
+
over_read?(read) || over_write?(written) || over_time?(time)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def over_read?(read)
|
|
34
|
+
read >= rekey_read_limit
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def over_write?(written)
|
|
38
|
+
written >= rekey_write_limit
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def over_time?(time)
|
|
42
|
+
(Time.now - time) >= rekey_time_limit
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Logger
|
|
5
|
+
class Filter
|
|
6
|
+
DEFAULT_FILTERED_KEYS = [
|
|
7
|
+
:password,
|
|
8
|
+
:passphrase,
|
|
9
|
+
:private_key,
|
|
10
|
+
:secret,
|
|
11
|
+
:key_blob,
|
|
12
|
+
:signature,
|
|
13
|
+
:shared_secret,
|
|
14
|
+
:session_id,
|
|
15
|
+
:mac_key,
|
|
16
|
+
:encryption_key,
|
|
17
|
+
:iv,
|
|
18
|
+
:nonce,
|
|
19
|
+
:token,
|
|
20
|
+
:credential,
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
FILTERED = "[FILTERED]"
|
|
24
|
+
BINARY_PREVIEW_BYTES = 4
|
|
25
|
+
|
|
26
|
+
def initialize(keys: DEFAULT_FILTERED_KEYS)
|
|
27
|
+
@keys = keys.map(&:to_s)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def apply(value, key: nil)
|
|
31
|
+
return FILTERED if key && filter_key?(key)
|
|
32
|
+
|
|
33
|
+
case value
|
|
34
|
+
when Hash
|
|
35
|
+
value.to_h { |k, v| [k, apply(v, key: k)] }
|
|
36
|
+
when Array
|
|
37
|
+
value.map { |v| apply(v) }
|
|
38
|
+
when String
|
|
39
|
+
binary?(value) ? format_binary(value) : value
|
|
40
|
+
else
|
|
41
|
+
value
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def filter_key?(key)
|
|
48
|
+
key_s = key.to_s.downcase
|
|
49
|
+
@keys.any? { |filtered| key_s.include?(filtered) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def binary?(value)
|
|
53
|
+
return false if value.empty?
|
|
54
|
+
return false if value.valid_encoding? && value.match?(/\A[[:print:]\s]*\z/)
|
|
55
|
+
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def format_binary(value)
|
|
60
|
+
preview = value.bytes.first(BINARY_PREVIEW_BYTES)
|
|
61
|
+
.map { |b| format("%02x", b) }
|
|
62
|
+
.join
|
|
63
|
+
"<#{value.bytesize} bytes: 0x#{preview}...>"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class << self
|
|
68
|
+
attr_writer :filter
|
|
69
|
+
|
|
70
|
+
def filter
|
|
71
|
+
@filter ||= Filter.new
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def debug(subject, message = nil, *args, **params)
|
|
75
|
+
Console.debug(subject, message, *args, **filter.apply(params))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def info(subject, message = nil, *args, **params)
|
|
79
|
+
Console.info(subject, message, *args, **filter.apply(params))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def warn(subject, message = nil, *args, **params)
|
|
83
|
+
Console.warn(subject, message, *args, **filter.apply(params))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def error(subject, message = nil, *args, **params)
|
|
87
|
+
Console.error(subject, message, *args, **filter.apply(params))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def fatal(subject, message = nil, *args, **params)
|
|
91
|
+
Console.fatal(subject, message, *args, **filter.apply(params))
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Mac
|
|
5
|
+
class Algorithm
|
|
6
|
+
def initialize(key)
|
|
7
|
+
@key = key
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def key_length = 0
|
|
11
|
+
def mac_length = 0
|
|
12
|
+
def etm? = false
|
|
13
|
+
|
|
14
|
+
def compute(sequence, data)
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def verify?(sequence, data, mac)
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Mac
|
|
5
|
+
class HmacShaAlgorithm < Algorithm
|
|
6
|
+
def initialize(key)
|
|
7
|
+
super
|
|
8
|
+
|
|
9
|
+
@key = key
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def compute(sequence, data)
|
|
13
|
+
OpenSSL::HMAC.digest(digest_name, @key, [sequence].pack("N") + data)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def verify?(sequence, data, mac)
|
|
17
|
+
expected = OpenSSL::HMAC.digest(digest_name, @key, [sequence].pack("N") + data)
|
|
18
|
+
OpenSSL.secure_compare(expected, mac)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def digest_name
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class HmacSha1 < HmacShaAlgorithm
|
|
29
|
+
NAME = "hmac-sha1"
|
|
30
|
+
|
|
31
|
+
def key_length = 20
|
|
32
|
+
def mac_length = 20
|
|
33
|
+
def digest_name = "SHA1"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class HmacSha256 < HmacShaAlgorithm
|
|
37
|
+
def key_length = 32
|
|
38
|
+
def mac_length = 32
|
|
39
|
+
def digest_name = "SHA256"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class HmacSha256Etm < HmacSha256
|
|
43
|
+
def etm? = true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class HmacSha512 < HmacShaAlgorithm
|
|
47
|
+
NAME = "hmac-sha2-512"
|
|
48
|
+
|
|
49
|
+
def key_length = 64
|
|
50
|
+
def mac_length = 64
|
|
51
|
+
def digest_name = "SHA512"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class HmacSha512Etm < HmacSha512
|
|
55
|
+
NAME = "hmac-sha2-512-etm@openssh.com"
|
|
56
|
+
|
|
57
|
+
def etm? = true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/crussh/mac.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Mac
|
|
5
|
+
HMAC_SHA256 = "hmac-sha2-256"
|
|
6
|
+
HMAC_SHA256_ETM = "hmac-sha2-256-etm@openssh.com"
|
|
7
|
+
HMAC_SHA512 = "hmac-sha2-512"
|
|
8
|
+
HMAC_SHA512_ETM = "hmac-sha2-512-etm@openssh.com"
|
|
9
|
+
NONE = "none"
|
|
10
|
+
|
|
11
|
+
DEFAULT = [
|
|
12
|
+
HMAC_SHA512_ETM,
|
|
13
|
+
HMAC_SHA256_ETM,
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
REGISTRY = {}
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def from_name(name, key)
|
|
20
|
+
algorithm_class = REGISTRY[name]
|
|
21
|
+
|
|
22
|
+
raise UnknownAlgorithm, "Unknown MAC algorithm: #{name}" if algorithm_class.nil?
|
|
23
|
+
|
|
24
|
+
algorithm_class.new(key)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
class Negotiator
|
|
5
|
+
def initialize(client_kexinit, server_kexinit)
|
|
6
|
+
@client_kexinit = client_kexinit
|
|
7
|
+
@server_kexinit = server_kexinit
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def negotiate
|
|
11
|
+
Algorithms.new(
|
|
12
|
+
kex: pick!(:kex_algorithms),
|
|
13
|
+
host_key: pick!(:server_host_key_algorithms),
|
|
14
|
+
cipher_client_to_server: pick!(:cipher_client_to_server),
|
|
15
|
+
cipher_server_to_client: pick!(:cipher_server_to_client),
|
|
16
|
+
mac_client_to_server: pick(:mac_client_to_server),
|
|
17
|
+
mac_server_to_client: pick(:mac_server_to_client),
|
|
18
|
+
compression_client_to_server: pick!(:compression_client_to_server),
|
|
19
|
+
compression_server_to_client: pick!(:compression_server_to_client),
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def pick!(category)
|
|
26
|
+
pick(category) || raise(NegotiationError, "No supported common algorithm for #{category}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def pick(category)
|
|
30
|
+
server_list, client_list = extract_lists(category)
|
|
31
|
+
|
|
32
|
+
return if client_list.empty? || server_list.empty?
|
|
33
|
+
|
|
34
|
+
client_list.find { |algorithm| server_list.include?(algorithm) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def extract_lists(category)
|
|
38
|
+
[@server_kexinit.send(category), @client_kexinit.send(category)]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
# Algorithm preferences - ordered by preference (first = most preferred)
|
|
5
|
+
class Preferred
|
|
6
|
+
def initialize
|
|
7
|
+
@kex = Kex::DEFAULT
|
|
8
|
+
@host_key = Keys::DEFAULT
|
|
9
|
+
@cipher = Cipher::DEFAULT
|
|
10
|
+
@mac = Mac::DEFAULT
|
|
11
|
+
@compression = Compression::DEFAULT
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_accessor :kex, :host_key, :cipher, :mac, :compression
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Protocol
|
|
5
|
+
class ChannelExtendedData < Message
|
|
6
|
+
message_type CHANNEL_EXTENDED_DATA
|
|
7
|
+
|
|
8
|
+
field :recipient_channel, :uint32
|
|
9
|
+
field :data_type_code, :uint32
|
|
10
|
+
field :data, :string
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Protocol
|
|
5
|
+
class ChannelOpen < Message
|
|
6
|
+
message_type CHANNEL_OPEN
|
|
7
|
+
|
|
8
|
+
field :channel_type, :string
|
|
9
|
+
field :sender_channel, :uint32
|
|
10
|
+
field :initial_window_size, :uint32
|
|
11
|
+
field :maximum_packet_size, :uint32
|
|
12
|
+
field :channel_data, :remaining
|
|
13
|
+
|
|
14
|
+
def x11?
|
|
15
|
+
channel_type == "x11"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def forwarded_tcpip?
|
|
19
|
+
channel_type == "forwarded-tcpip"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def direct_tcpip?
|
|
23
|
+
channel_type == "direct-tcpip"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def x11
|
|
27
|
+
return @x11 if @x11
|
|
28
|
+
return unless x11?
|
|
29
|
+
|
|
30
|
+
reader = Transport::Reader.new(channel_data)
|
|
31
|
+
|
|
32
|
+
originator_address = reader.string
|
|
33
|
+
originator_port = reader.uint32
|
|
34
|
+
|
|
35
|
+
@x11 = X11.new(originator_address:, originator_port:)
|
|
36
|
+
end
|
|
37
|
+
X11 = Data.define(:originator_address, :originator_port)
|
|
38
|
+
|
|
39
|
+
def forwarded_tcpip
|
|
40
|
+
return @forwarded_tcpip if @forwarded_tcpip
|
|
41
|
+
return unless forwarded_tcpip?
|
|
42
|
+
|
|
43
|
+
reader = Transport::Reader.new(channel_data)
|
|
44
|
+
|
|
45
|
+
address = reader.string
|
|
46
|
+
port = reader.uint32
|
|
47
|
+
originator_address = reader.string
|
|
48
|
+
originator_port = reader.uint32
|
|
49
|
+
|
|
50
|
+
@forwarded_tcpip = Tcpip.new(address:, port:, originator_address:, originator_port:)
|
|
51
|
+
end
|
|
52
|
+
Tcpip = Data.define(:address, :port, :originator_address, :originator_port)
|
|
53
|
+
|
|
54
|
+
def direct_tcpip
|
|
55
|
+
return @direct_tcpip if @direct_tcpip
|
|
56
|
+
return unless direct_tcpip?
|
|
57
|
+
|
|
58
|
+
reader = Transport::Reader.new(channel_data)
|
|
59
|
+
|
|
60
|
+
address = reader.string
|
|
61
|
+
port = reader.uint32
|
|
62
|
+
originator_address = reader.string
|
|
63
|
+
originator_port = reader.uint32
|
|
64
|
+
|
|
65
|
+
@direct_tcpip = Tcpip.new(address:, port:, originator_address:, originator_port:)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Protocol
|
|
5
|
+
class ChannelOpenConfirmation < Message
|
|
6
|
+
message_type CHANNEL_OPEN_CONFIRMATION
|
|
7
|
+
|
|
8
|
+
field :recipient_channel, :uint32
|
|
9
|
+
field :sender_channel, :uint32
|
|
10
|
+
field :initial_window_size, :uint32
|
|
11
|
+
field :maximum_packet_size, :uint32
|
|
12
|
+
field :channel_data, :remaining, default: ""
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Protocol
|
|
5
|
+
class ChannelOpenFailure < Message
|
|
6
|
+
message_type CHANNEL_OPEN_FAILURE
|
|
7
|
+
|
|
8
|
+
field :recipient_channel, :uint32
|
|
9
|
+
field :reason_code, :uint32
|
|
10
|
+
field :description, :string
|
|
11
|
+
field :language, :string, default: ""
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|