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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +1 -0
  3. data/.travis.yml +4 -4
  4. data/Gemfile +5 -1
  5. data/README.md +124 -63
  6. data/lib/netsnmp.rb +66 -10
  7. data/lib/netsnmp/client.rb +93 -75
  8. data/lib/netsnmp/encryption/aes.rb +84 -0
  9. data/lib/netsnmp/encryption/des.rb +80 -0
  10. data/lib/netsnmp/encryption/none.rb +17 -0
  11. data/lib/netsnmp/errors.rb +1 -3
  12. data/lib/netsnmp/message.rb +81 -0
  13. data/lib/netsnmp/oid.rb +18 -137
  14. data/lib/netsnmp/pdu.rb +106 -64
  15. data/lib/netsnmp/scoped_pdu.rb +23 -0
  16. data/lib/netsnmp/security_parameters.rb +198 -0
  17. data/lib/netsnmp/session.rb +84 -275
  18. data/lib/netsnmp/v3_session.rb +81 -0
  19. data/lib/netsnmp/varbind.rb +65 -156
  20. data/lib/netsnmp/version.rb +2 -1
  21. data/netsnmp.gemspec +2 -8
  22. data/spec/client_spec.rb +147 -99
  23. data/spec/handlers/celluloid_spec.rb +33 -20
  24. data/spec/oid_spec.rb +11 -5
  25. data/spec/pdu_spec.rb +22 -22
  26. data/spec/security_parameters_spec.rb +40 -0
  27. data/spec/session_spec.rb +0 -23
  28. data/spec/support/celluloid.rb +24 -0
  29. data/spec/support/request_examples.rb +36 -0
  30. data/spec/support/start_docker.sh +15 -1
  31. data/spec/v3_session_spec.rb +21 -0
  32. data/spec/varbind_spec.rb +2 -51
  33. metadata +30 -76
  34. data/lib/netsnmp/core.rb +0 -12
  35. data/lib/netsnmp/core/client.rb +0 -15
  36. data/lib/netsnmp/core/constants.rb +0 -153
  37. data/lib/netsnmp/core/inline.rb +0 -20
  38. data/lib/netsnmp/core/libc.rb +0 -48
  39. data/lib/netsnmp/core/libsnmp.rb +0 -44
  40. data/lib/netsnmp/core/structures.rb +0 -167
  41. data/lib/netsnmp/core/utilities.rb +0 -13
  42. data/lib/netsnmp/handlers/celluloid.rb +0 -27
  43. data/lib/netsnmp/handlers/em.rb +0 -56
  44. data/spec/core/libc_spec.rb +0 -2
  45. data/spec/core/libsnmp_spec.rb +0 -32
  46. data/spec/core/structures_spec.rb +0 -54
  47. 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
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ module NETSNMP
3
+ module Encryption
4
+ class None
5
+
6
+ def initialize(*)
7
+ end
8
+
9
+ def encrypt(pdu)
10
+ pdu.send(:to_asn)
11
+ end
12
+ def decrypt(stream, *)
13
+ stream
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,8 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  module NETSNMP
2
3
  Error = Class.new(StandardError)
3
4
  ConnectionFailed = Class.new(Error)
4
5
  AuthenticationFailed = Class.new(Error)
5
-
6
- SendError = Class.new(Error)
7
- ReceiveError = Class.new(Error)
8
6
  end
@@ -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
- class OID
5
- Error = Class.new(Error)
5
+ module OID
6
6
  OIDREGEX = /^[\d\.]*$/
7
7
 
8
- class << self
9
-
10
- # @return [Integer] the default oid size in bytes
11
- def default_size
12
- @default_size ||= Core::Inline.oid_size
13
- end
14
-
15
- # @param [FFI::Pointer] pointer the pointer to the beginning ot the memory octet
16
- # @param [Integer] length the length of the oid
17
- # @return [String] the oid code (ex: "1.2.4.56.3.4.5"...)
18
- #
19
- def read_pointer(pointer, length)
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
- attr_reader :code
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 parent_of?(child_oid)
69
- child_code = child_oid.is_a?(OID) ? child_oid.code : child_oid
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