netsnmp 0.0.2 → 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 +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
|