netsnmp 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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