netsnmp 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +1 -0
- data/.travis.yml +4 -4
- data/Gemfile +5 -1
- data/README.md +124 -63
- data/lib/netsnmp.rb +66 -10
- data/lib/netsnmp/client.rb +93 -75
- data/lib/netsnmp/encryption/aes.rb +84 -0
- data/lib/netsnmp/encryption/des.rb +80 -0
- data/lib/netsnmp/encryption/none.rb +17 -0
- data/lib/netsnmp/errors.rb +1 -3
- data/lib/netsnmp/message.rb +81 -0
- data/lib/netsnmp/oid.rb +18 -137
- data/lib/netsnmp/pdu.rb +106 -64
- data/lib/netsnmp/scoped_pdu.rb +23 -0
- data/lib/netsnmp/security_parameters.rb +198 -0
- data/lib/netsnmp/session.rb +84 -275
- data/lib/netsnmp/v3_session.rb +81 -0
- data/lib/netsnmp/varbind.rb +65 -156
- data/lib/netsnmp/version.rb +2 -1
- data/netsnmp.gemspec +2 -8
- data/spec/client_spec.rb +147 -99
- data/spec/handlers/celluloid_spec.rb +33 -20
- data/spec/oid_spec.rb +11 -5
- data/spec/pdu_spec.rb +22 -22
- data/spec/security_parameters_spec.rb +40 -0
- data/spec/session_spec.rb +0 -23
- data/spec/support/celluloid.rb +24 -0
- data/spec/support/request_examples.rb +36 -0
- data/spec/support/start_docker.sh +15 -1
- data/spec/v3_session_spec.rb +21 -0
- data/spec/varbind_spec.rb +2 -51
- metadata +30 -76
- data/lib/netsnmp/core.rb +0 -12
- data/lib/netsnmp/core/client.rb +0 -15
- data/lib/netsnmp/core/constants.rb +0 -153
- data/lib/netsnmp/core/inline.rb +0 -20
- data/lib/netsnmp/core/libc.rb +0 -48
- data/lib/netsnmp/core/libsnmp.rb +0 -44
- data/lib/netsnmp/core/structures.rb +0 -167
- data/lib/netsnmp/core/utilities.rb +0 -13
- data/lib/netsnmp/handlers/celluloid.rb +0 -27
- data/lib/netsnmp/handlers/em.rb +0 -56
- data/spec/core/libc_spec.rb +0 -2
- data/spec/core/libsnmp_spec.rb +0 -32
- data/spec/core/structures_spec.rb +0 -54
- data/spec/handlers/em_client_spec.rb +0 -34
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module NETSNMP
|
3
|
+
module Encryption
|
4
|
+
class AES
|
5
|
+
def initialize(priv_key, local: 0)
|
6
|
+
@priv_key = priv_key
|
7
|
+
@local = local
|
8
|
+
end
|
9
|
+
|
10
|
+
def encrypt(decrypted_data, engine_boots: , engine_time: )
|
11
|
+
cipher = OpenSSL::Cipher::AES128.new(:CFB)
|
12
|
+
|
13
|
+
iv, salt = generate_encryption_key(engine_boots, engine_time)
|
14
|
+
|
15
|
+
cipher.encrypt
|
16
|
+
cipher.iv = iv
|
17
|
+
cipher.key = des_key
|
18
|
+
|
19
|
+
if (diff = decrypted_data.length % 8) != 0
|
20
|
+
decrypted_data << ("\x00" * (8 - diff))
|
21
|
+
end
|
22
|
+
|
23
|
+
encrypted_data = cipher.update(decrypted_data) + cipher.final
|
24
|
+
NETSNMP.debug { "encrypted:\n#{Hexdump.dump(encrypted_data)}" }
|
25
|
+
|
26
|
+
[encrypted_data, salt]
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def decrypt(encrypted_data, salt: , engine_boots: , engine_time: )
|
31
|
+
raise Error, "invalid priv salt received" unless salt.length % 8 == 0
|
32
|
+
raise Error, "invalid encrypted PDU received" unless encrypted_data.length % 8 == 0
|
33
|
+
|
34
|
+
cipher = OpenSSL::Cipher::AES128.new(:CFB)
|
35
|
+
cipher.padding = 0
|
36
|
+
|
37
|
+
iv = generate_decryption_key(engine_boots, engine_time, salt)
|
38
|
+
|
39
|
+
cipher.decrypt
|
40
|
+
cipher.key = des_key
|
41
|
+
cipher.iv = iv
|
42
|
+
decrypted_data = cipher.update(encrypted_data) + cipher.final
|
43
|
+
NETSNMP.debug {"decrypted:\n#{Hexdump.dump(decrypted_data)}" }
|
44
|
+
|
45
|
+
hlen, bodylen = OpenSSL::ASN1.traverse(decrypted_data) { |_, _, x, y, *| break x, y }
|
46
|
+
decrypted_data.byteslice(0, hlen+bodylen)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
# 8.1.1.1
|
51
|
+
def generate_encryption_key(boots, time)
|
52
|
+
salt = [0xff & (@local >> 56),
|
53
|
+
0xff & (@local >> 48),
|
54
|
+
0xff & (@local >> 40),
|
55
|
+
0xff & (@local >> 32),
|
56
|
+
0xff & (@local >> 24),
|
57
|
+
0xff & (@local >> 16),
|
58
|
+
0xff & (@local >> 8),
|
59
|
+
0xff & @local].pack("c*")
|
60
|
+
@local = @local == 0xffffffffffffffff ? 0 : @local + 1
|
61
|
+
|
62
|
+
iv = generate_decryption_key(boots, time, salt)
|
63
|
+
|
64
|
+
[iv, salt]
|
65
|
+
end
|
66
|
+
|
67
|
+
def generate_decryption_key(boots, time, salt)
|
68
|
+
[0xff & (boots >> 24),
|
69
|
+
0xff & (boots >> 16),
|
70
|
+
0xff & (boots >> 8),
|
71
|
+
0xff & boots,
|
72
|
+
0xff & (time >> 24),
|
73
|
+
0xff & (time >> 16),
|
74
|
+
0xff & (time >> 8),
|
75
|
+
0xff & time].pack("c*") + salt
|
76
|
+
end
|
77
|
+
|
78
|
+
def des_key
|
79
|
+
@priv_key[0,16]
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module NETSNMP
|
3
|
+
module Encryption
|
4
|
+
using StringExtensions
|
5
|
+
|
6
|
+
class DES
|
7
|
+
def initialize(priv_key, local: 0)
|
8
|
+
@priv_key = priv_key
|
9
|
+
@local = local
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def encrypt(decrypted_data, engine_boots: , engine_time: nil)
|
14
|
+
cipher = OpenSSL::Cipher::DES.new(:CBC)
|
15
|
+
|
16
|
+
iv, salt = generate_encryption_key(engine_boots)
|
17
|
+
|
18
|
+
cipher.encrypt
|
19
|
+
cipher.iv = iv
|
20
|
+
cipher.key = des_key
|
21
|
+
|
22
|
+
if (diff = decrypted_data.length % 8) != 0
|
23
|
+
decrypted_data << ("\x00" * (8 - diff))
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
encrypted_data = cipher.update(decrypted_data) + cipher.final
|
28
|
+
NETSNMP.debug {"encrypted:\n#{Hexdump.dump(encrypted_data)}" }
|
29
|
+
[encrypted_data, salt]
|
30
|
+
end
|
31
|
+
|
32
|
+
def decrypt(encrypted_data, salt: , engine_boots: nil, engine_time: nil)
|
33
|
+
raise Error, "invalid priv salt received" unless salt.length % 8 == 0
|
34
|
+
raise Error, "invalid encrypted PDU received" unless encrypted_data.length % 8 == 0
|
35
|
+
|
36
|
+
cipher = OpenSSL::Cipher::DES.new(:CBC)
|
37
|
+
cipher.padding = 0
|
38
|
+
|
39
|
+
iv = generate_decryption_key(salt)
|
40
|
+
|
41
|
+
cipher.decrypt
|
42
|
+
cipher.key = des_key
|
43
|
+
cipher.iv = iv
|
44
|
+
decrypted_data = cipher.update(encrypted_data) + cipher.final
|
45
|
+
NETSNMP.debug {"decrypted:\n#{Hexdump.dump(decrypted_data)}" }
|
46
|
+
|
47
|
+
hlen, bodylen = OpenSSL::ASN1.traverse(decrypted_data) { |_, _, x, y, *| break x, y }
|
48
|
+
decrypted_data.byteslice(0, hlen+bodylen)
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
private
|
53
|
+
# 8.1.1.1
|
54
|
+
def generate_encryption_key(boots)
|
55
|
+
pre_iv = @priv_key[8,8]
|
56
|
+
salt = [0xff & (boots >> 24),
|
57
|
+
0xff & (boots >> 16),
|
58
|
+
0xff & (boots >> 8),
|
59
|
+
0xff & boots,
|
60
|
+
0xff & (@local >> 24),
|
61
|
+
0xff & (@local >> 16),
|
62
|
+
0xff & (@local >> 8),
|
63
|
+
0xff & @local].pack("c*")
|
64
|
+
@local = @local == 0xffffffff ? 0 : @local + 1
|
65
|
+
|
66
|
+
iv = pre_iv.xor(salt)
|
67
|
+
[iv, salt]
|
68
|
+
end
|
69
|
+
|
70
|
+
def generate_decryption_key(salt)
|
71
|
+
pre_iv = @priv_key[8,8]
|
72
|
+
pre_iv.xor(salt)
|
73
|
+
end
|
74
|
+
|
75
|
+
def des_key
|
76
|
+
@priv_key[0,8]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/netsnmp/errors.rb
CHANGED
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module NETSNMP
|
3
|
+
# Factory for the SNMP v3 Message format
|
4
|
+
module Message
|
5
|
+
extend self
|
6
|
+
AUTHNONE = OpenSSL::ASN1::OctetString.new("\x00" * 12)
|
7
|
+
PRIVNONE = OpenSSL::ASN1::OctetString.new("")
|
8
|
+
MSG_MAX_SIZE = OpenSSL::ASN1::Integer.new(65507)
|
9
|
+
MSG_SECURITY_MODEL = OpenSSL::ASN1::Integer.new(3) # usmSecurityModel
|
10
|
+
MSG_VERSION = OpenSSL::ASN1::Integer.new(3)
|
11
|
+
MSG_REPORTABLE = 4
|
12
|
+
|
13
|
+
# @param [String] payload of an snmp v3 message which can be decoded
|
14
|
+
# @param [NETSMP::SecurityParameters, #decode] security_parameters knowns how to decode the stream
|
15
|
+
#
|
16
|
+
# @return [NETSNMP::ScopedPDU] the decoded PDU
|
17
|
+
#
|
18
|
+
def decode(stream, security_parameters: )
|
19
|
+
asn_tree = OpenSSL::ASN1.decode(stream)
|
20
|
+
version, headers, sec_params, pdu_payload = asn_tree.value
|
21
|
+
|
22
|
+
sec_params_asn = OpenSSL::ASN1.decode(sec_params.value).value
|
23
|
+
|
24
|
+
engine_id, engine_boots, engine_time, username, auth_param, priv_param = sec_params_asn.map(&:value)
|
25
|
+
|
26
|
+
# validate_authentication
|
27
|
+
security_parameters.verify(stream.sub(auth_param, AUTHNONE.value), auth_param)
|
28
|
+
|
29
|
+
engine_boots=engine_boots.to_i
|
30
|
+
engine_time =engine_time.to_i
|
31
|
+
|
32
|
+
encoded_pdu = security_parameters.decode(pdu_payload, salt: priv_param,
|
33
|
+
engine_boots: engine_boots,
|
34
|
+
engine_time: engine_time)
|
35
|
+
|
36
|
+
pdu = ScopedPDU.decode(encoded_pdu)
|
37
|
+
[pdu, engine_id, engine_boots, engine_time]
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param [NETSNMP::ScopedPDU] the PDU to encode in the message
|
41
|
+
# @param [NETSMP::SecurityParameters, #decode] security_parameters knowns how to decode the stream
|
42
|
+
#
|
43
|
+
# @return [String] the byte representation of an SNMP v3 Message
|
44
|
+
#
|
45
|
+
def encode(pdu, security_parameters: , engine_boots: 0, engine_time: 0)
|
46
|
+
scoped_pdu, salt_param = security_parameters.encode(pdu, salt: PRIVNONE,
|
47
|
+
engine_boots: engine_boots,
|
48
|
+
engine_time: engine_time)
|
49
|
+
|
50
|
+
sec_params = OpenSSL::ASN1::Sequence.new([
|
51
|
+
OpenSSL::ASN1::OctetString.new(security_parameters.engine_id),
|
52
|
+
OpenSSL::ASN1::Integer.new(engine_boots),
|
53
|
+
OpenSSL::ASN1::Integer.new(engine_time),
|
54
|
+
OpenSSL::ASN1::OctetString.new(security_parameters.username),
|
55
|
+
AUTHNONE,
|
56
|
+
salt_param
|
57
|
+
])
|
58
|
+
message_flags = MSG_REPORTABLE | security_parameters.security_level
|
59
|
+
message_id = OpenSSL::ASN1::Integer.new(SecureRandom.random_number(2147483647))
|
60
|
+
headers = OpenSSL::ASN1::Sequence.new([
|
61
|
+
message_id, MSG_MAX_SIZE,
|
62
|
+
OpenSSL::ASN1::OctetString.new( [String(message_flags)].pack("h*") ),
|
63
|
+
MSG_SECURITY_MODEL
|
64
|
+
])
|
65
|
+
|
66
|
+
encoded = OpenSSL::ASN1::Sequence([
|
67
|
+
MSG_VERSION,
|
68
|
+
headers,
|
69
|
+
OpenSSL::ASN1::OctetString.new(sec_params.to_der),
|
70
|
+
scoped_pdu
|
71
|
+
]).to_der
|
72
|
+
signature = security_parameters.sign(encoded)
|
73
|
+
if signature
|
74
|
+
auth_salt = OpenSSL::ASN1::OctetString.new(signature)
|
75
|
+
encoded.sub!(AUTHNONE.to_der, auth_salt.to_der)
|
76
|
+
end
|
77
|
+
encoded
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
data/lib/netsnmp/oid.rb
CHANGED
@@ -1,153 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module NETSNMP
|
2
3
|
# Abstracts the OID structure
|
3
4
|
#
|
4
|
-
|
5
|
-
Error = Class.new(Error)
|
5
|
+
module OID
|
6
6
|
OIDREGEX = /^[\d\.]*$/
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
#
|
19
|
-
|
20
|
-
pointer.__send__(:"read_array_of_uint#{default_size * 8}", length).join('.')
|
21
|
-
end
|
22
|
-
|
23
|
-
# @see read_pointer
|
24
|
-
# @return [OID] an OID object initialized from a code read from memory
|
25
|
-
#
|
26
|
-
def from_pointer(pointer, length)
|
27
|
-
new(read_pointer(pointer, length))
|
8
|
+
extend self
|
9
|
+
|
10
|
+
def build(o)
|
11
|
+
case o
|
12
|
+
when OID then o
|
13
|
+
when Array
|
14
|
+
o.join('.')
|
15
|
+
when OIDREGEX
|
16
|
+
o = o[1..-1] if o.start_with?('.')
|
17
|
+
o
|
18
|
+
# TODO: MIB to OID
|
19
|
+
else raise Error, "can't convert #{o} to OID"
|
28
20
|
end
|
29
|
-
|
30
21
|
end
|
31
22
|
|
32
|
-
|
33
|
-
|
34
|
-
# @param [String] code the oid code
|
35
|
-
#
|
36
|
-
def initialize(code)
|
37
|
-
@struct = FFI::MemoryPointer.new(OID::default_size * Core::Constants::MAX_OID_LEN)
|
38
|
-
@length_pointer = FFI::MemoryPointer.new(:size_t)
|
39
|
-
@length_pointer.write_int(Core::Constants::MAX_OID_LEN)
|
40
|
-
existing_oid = case code
|
41
|
-
when OIDREGEX then Core::LibSNMP.read_objid(code, @struct, @length_pointer)
|
42
|
-
else Core::LibSNMP.get_node(code, @struct, @length_pointer)
|
43
|
-
end
|
44
|
-
raise Error, "unsupported oid: #{code}" if existing_oid.zero?
|
23
|
+
def to_asn(oid)
|
24
|
+
OpenSSL::ASN1::ObjectId.new(oid)
|
45
25
|
end
|
46
26
|
|
47
|
-
# @return [String] the oid code
|
48
|
-
#
|
49
|
-
def code ; @code ||= OID.read_pointer(pointer, length) ; end
|
50
|
-
|
51
|
-
# @return [String] the pointer to the structure
|
52
|
-
#
|
53
|
-
def pointer ; @struct ; end
|
54
|
-
|
55
|
-
# @return [Integer] length of the oid
|
56
|
-
#
|
57
|
-
def length ; @length_pointer.read_int ; end
|
58
|
-
|
59
|
-
# @return [Integer] size of the oid
|
60
|
-
#
|
61
|
-
def size ; length * NETSNMP::OID.default_size ; end
|
62
|
-
|
63
|
-
def to_s ; code ; end
|
64
|
-
|
65
27
|
# @param [OID, String] child oid another oid
|
66
28
|
# @return [true, false] whether the given OID belongs to the sub-tree
|
67
29
|
#
|
68
|
-
def
|
69
|
-
|
70
|
-
child_code.start_with?(code)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
# SNMP v3-relevant OIDs
|
75
|
-
class AuthOID < OID
|
76
|
-
def generate_key(session, user, pass)
|
77
|
-
raise Error, "no given Authorization User" unless user
|
78
|
-
raise Error, "no given Authorization Password" unless pass
|
79
|
-
|
80
|
-
session[:securityAuthProto] = pointer
|
81
|
-
session[:securityName] = FFI::MemoryPointer.from_string(user)
|
82
|
-
session[:securityNameLen] = user.length
|
83
|
-
|
84
|
-
auth_len_ptr = FFI::MemoryPointer.new(:size_t)
|
85
|
-
auth_len_ptr.write_int(Core::Constants::USM_AUTH_KU_LEN)
|
86
|
-
|
87
|
-
auth_key_result = Core::LibSNMP.generate_Ku(pointer,
|
88
|
-
session[:securityAuthProtoLen],
|
89
|
-
pass,
|
90
|
-
pass.length,
|
91
|
-
session[:securityAuthKey],
|
92
|
-
auth_len_ptr)
|
93
|
-
unless auth_key_result == Core::Constants::SNMPERR_SUCCESS
|
94
|
-
raise AuthenticationFailed, "failed to authenticate #{auth_user} in #{@host}"
|
95
|
-
end
|
96
|
-
session[:securityAuthKeyLen] = auth_len_ptr.read_int
|
30
|
+
def parent?(parent_oid, child_oid)
|
31
|
+
child_oid.match(%r/\A#{parent_oid}\./)
|
97
32
|
end
|
98
33
|
end
|
99
|
-
|
100
|
-
class PrivOID < OID
|
101
|
-
|
102
|
-
def generate_key(session, user, pass)
|
103
|
-
raise Error, "no given Priv User" unless user
|
104
|
-
raise Error, "no given Priv Password" unless pass
|
105
|
-
|
106
|
-
session[:securityPrivProto] = pointer
|
107
|
-
|
108
|
-
# other necessary lengths
|
109
|
-
priv_len_ptr = FFI::MemoryPointer.new(:size_t)
|
110
|
-
priv_len_ptr.write_int(Core::Constants::USM_PRIV_KU_LEN)
|
111
|
-
|
112
|
-
# NOTE I know this is handing off the AuthProto, but generates a proper
|
113
|
-
# key for encryption, and using PrivProto does not.
|
114
|
-
priv_key_result = Core::LibSNMP.generate_Ku(session[:securityAuthProto],
|
115
|
-
session[:securityAuthProtoLen],
|
116
|
-
pass,
|
117
|
-
pass.length,
|
118
|
-
session[:securityPrivKey],
|
119
|
-
priv_len_ptr)
|
120
|
-
|
121
|
-
unless priv_key_result == Core::Constants::SNMPERR_SUCCESS
|
122
|
-
raise AuthenticationFailed, "failed to authenticate #{auth_user} in #{@host}"
|
123
|
-
end
|
124
|
-
session[:securityPrivKeyLen] = priv_len_ptr.read_int
|
125
|
-
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
class MD5OID < AuthOID
|
130
|
-
def initialize ; super("1.3.6.1.6.3.10.1.1.2") ; end
|
131
|
-
end
|
132
|
-
class SHA1OID < AuthOID
|
133
|
-
def initialize ; super("1.3.6.1.6.3.10.1.1.3") ; end
|
134
|
-
end
|
135
|
-
class NoAuthOID < AuthOID
|
136
|
-
def initialize ; super("1.3.6.1.6.3.10.1.1.1") ; end
|
137
|
-
def generate_key(session, *)
|
138
|
-
session[:securityAuthProto] = pointer
|
139
|
-
end
|
140
|
-
end
|
141
|
-
class AESOID < PrivOID
|
142
|
-
def initialize ; super("1.3.6.1.6.3.10.1.2.4") ; end
|
143
|
-
end
|
144
|
-
class DESOID < PrivOID
|
145
|
-
def initialize ; super("1.3.6.1.6.3.10.1.2.2") ; end
|
146
|
-
end
|
147
|
-
class NoPrivOID < PrivOID
|
148
|
-
def initialize ; super("1.3.6.1.6.3.10.1.2.1") ; end
|
149
|
-
def generate_key(session, *)
|
150
|
-
session[:securityPrivProto] = pointer
|
151
|
-
end
|
152
|
-
end
|
153
34
|
end
|