packetgen-plugin-ipsec 1.0.2 → 1.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.
@@ -1,261 +1,261 @@
1
1
  # coding: utf-8
2
+ # frozen_string_literal: true
3
+
2
4
  # This file is part of IPsec packetgen plugin.
3
5
  # See https://github.com/sdaubert/packetgen-plugin-ipsec for more informations
4
6
  # Copyright (c) 2018 Sylvain Daubert <sylvain.daubert@laposte.net>
5
7
  # This program is published under MIT license.
6
8
 
7
- # frozen_string_literal: true
9
+ module PacketGen::Plugin
10
+ class IKE
11
+ # This class handles encrypted payloads, denoted SK.
12
+ #
13
+ # The encrypted payload contains other payloads in encrypted form.
14
+ # The Encrypted payload consists of the IKE generic payload Plugin followed
15
+ # by individual fields as follows:
16
+ # 1 2 3
17
+ # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
18
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
19
+ # | Next Payload |C| RESERVED | Payload Length |
20
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
21
+ # | Initialization Vector |
22
+ # | (length is block size for encryption algorithm) |
23
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
24
+ # ~ Encrypted IKE Payloads ~
25
+ # + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
26
+ # | | Padding (0-255 octets) |
27
+ # +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
28
+ # | | Pad Length |
29
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
30
+ # ~ Integrity Checksum Data ~
31
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
32
+ # Encrypted payloads are set in {#content} field, as a {BinStruct::String}.
33
+ # All others fields are only set when decrypting a previously read SK
34
+ # payload. They also may be set manually to encrypt IKE payloads.
35
+ #
36
+ # == Read and decrypt a SK payload
37
+ # # Read a IKE packet
38
+ # pkt = PacketGen.read(str)
39
+ # # decrypt SK payload
40
+ # cipher = OpenSSL::Cipher.new('aes-128-ctr')
41
+ # cipher.decrypt
42
+ # cipher_key = aes_key
43
+ # hmac = OpenSSL::HMAC.new(hmac_key, OpenSSL::Digest::SHA256.new)
44
+ # pkt.ike_sk.decrypt! cipher, intmode: hmac, icv_length: 16 # => true if authentication is verified
45
+ # pkt.ike_sk.body # => kind of PacketGen::Plugin::IKE::Payload
46
+ #
47
+ # == Set and encrypt a SK payload
48
+ # # Create a IKE packet
49
+ # pkt = PacketGen.gen('IP').add('IP').add('UDP').add('IKE', init_spi: 0x123456789, resp_spi: 0x987654321, type: 'IKE_AUTH', message_id: 1)
50
+ # # Add SK payload
51
+ # pkt.add('IKE::SK', icv_length: 16)
52
+ # # Add others unencrypted payloads
53
+ # pkt.add('IKE::IDi').add('IKE::Auth').add('IKE::SA').add('IKE::TSi').add('IKE::TSr')
54
+ # # encrypt SK payload
55
+ # cipher = OpenSSL::Cipher.new('aes-128-ctr')
56
+ # cipher.encrypt
57
+ # cipher_key = aes_key
58
+ # hmac = OpenSSL::HMAC.new(hmac_key, OpenSSL::Digest::SHA256.new)
59
+ # pkt.ike_sk.encrypt! cipher, iv, salt: salt, intmode: hmac
60
+ # pkt.ike_sk.body # => String
61
+ # pkt.calc_length
62
+ #
63
+ # @author Sylvain Daubert
64
+ class SK < Payload
65
+ include Crypto
8
66
 
9
- module PacketGen
10
- module Plugin
11
- class IKE
12
- # This class handles encrypted payloads, denoted SK.
13
- #
14
- # The encrypted payload contains other payloads in encrypted form.
15
- # The Encrypted payload consists of the IKE generic payload Plugin followed
16
- # by individual fields as follows:
17
- # 1 2 3
18
- # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
19
- # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20
- # | Next Payload |C| RESERVED | Payload Length |
21
- # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
22
- # | Initialization Vector |
23
- # | (length is block size for encryption algorithm) |
24
- # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
25
- # ~ Encrypted IKE Payloads ~
26
- # + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
27
- # | | Padding (0-255 octets) |
28
- # +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
29
- # | | Pad Length |
30
- # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
31
- # ~ Integrity Checksum Data ~
32
- # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
33
- # Encrypted payloads are set in {#content} field, as a {PacketGen::Types::String}.
34
- # All others fields are only set when decrypting a previously read SK
35
- # payload. They also may be set manually to encrypt IKE payloads.
36
- #
37
- # == Read and decrypt a SK payload
38
- # # Read a IKE packet
39
- # pkt = PacketGen.read(str)
40
- # # decrypt SK payload
41
- # cipher = OpenSSL::Cipher.new('aes-128-ctr')
42
- # cipher.decrypt
43
- # cipher_key = aes_key
44
- # hmac = OpenSSL::HMAC.new(hmac_key, OpenSSL::Digest::SHA256.new)
45
- # pkt.ike_sk.decrypt! cipher, intmode: hmac, icv_length: 16 # => true if authentication is verified
46
- # pkt.ike_sk.body # => kind of PacketGen::Plugin::IKE::Payload
47
- #
48
- # == Set and encrypt a SK payload
49
- # # Create a IKE packet
50
- # pkt = PacketGen.gen('IP').add('IP').add('UDP').add('IKE', init_spi: 0x123456789, resp_spi: 0x987654321, type: 'IKE_AUTH', message_id: 1)
51
- # # Add SK payload
52
- # pkt.add('IKE::SK', icv_length: 16)
53
- # # Add others unencrypted payloads
54
- # pkt.add('IKE::IDi').add('IKE::Auth').add('IKE::SA').add('IKE::TSi').add('IKE::TSr')
55
- # # encrypt SK payload
56
- # cipher = OpenSSL::Cipher.new('aes-128-ctr')
57
- # cipher.encrypt
58
- # cipher_key = aes_key
59
- # hmac = OpenSSL::HMAC.new(hmac_key, OpenSSL::Digest::SHA256.new)
60
- # pkt.ike_sk.encrypt! cipher, iv, salt: salt, intmode: hmac
61
- # pkt.ike_sk.body # => String
62
- # pkt.calc_length
63
- #
64
- # @author Sylvain Daubert
65
- class SK < Payload
66
- include Crypto
67
+ # Payload type number
68
+ PAYLOAD_TYPE = 46
67
69
 
68
- # Payload type number
69
- PAYLOAD_TYPE = 46
70
+ # ICV (Integrity Check Value) length
71
+ # @return [Integer]
72
+ attr_accessor :icv_length
70
73
 
71
- # ICV (Integrity Check Value) length
72
- # @return [Integer]
73
- attr_accessor :icv_length
74
-
75
- # @param [Hash] options
76
- # @option options [Integer] :icv_length ICV length
77
- def initialize(options={})
78
- @icv_length = options[:icv_length] || 0
79
- super
80
- end
74
+ # @param [Hash] options
75
+ # @option options [Integer] :icv_length ICV length
76
+ def initialize(options={})
77
+ @icv_length = options[:icv_length] || 0
78
+ super
79
+ end
81
80
 
82
- # Decrypt in-place SK payload.
83
- # @param [OpenSSL::Cipher] cipher keyed cipher
84
- # This cipher is confidentiality-only one, or AEAD one. To use a second
85
- # cipher to add integrity, use +:intmode+ option.
86
- # @param [Hash] options
87
- # @option options [Boolean] :parse parse deciphered payload to retrieve
88
- # Plugins (default: +true+)
89
- # @option options [Fixnum] :icv_length ICV length for captured packets,
90
- # or read from PCapNG files
91
- # @option options [String] :salt salt value for CTR and GCM modes
92
- # @option options [OpenSSL::HMAC] :intmode integrity mode to use with a
93
- # confidentiality-only cipher. Only HMAC are supported.
94
- # @return [Boolean] +true+ if SK payload is authenticated
95
- def decrypt!(cipher, options={})
96
- opt = { salt: '', parse: true }.merge!(options)
81
+ # Decrypt in-place SK payload.
82
+ # @param [OpenSSL::Cipher] cipher keyed cipher
83
+ # This cipher is confidentiality-only one, or AEAD one. To use a second
84
+ # cipher to add integrity, use +:intmode+ option.
85
+ # @param [Hash] options
86
+ # @option options [Boolean] :parse parse deciphered payload to retrieve
87
+ # Plugins (default: +true+)
88
+ # @option options [Fixnum] :icv_length ICV length for captured packets,
89
+ # or read from PCapNG files
90
+ # @option options [String] :salt salt value for CTR and GCM modes
91
+ # @option options [OpenSSL::HMAC] :intmode integrity mode to use with a
92
+ # confidentiality-only cipher. Only HMAC are supported.
93
+ # @return [Boolean] +true+ if SK payload is authenticated
94
+ def decrypt!(cipher, options={})
95
+ opt = { salt: '', parse: true }.merge!(options)
97
96
 
98
- set_crypto cipher, opt[:intmode]
97
+ set_crypto cipher, opt[:intmode]
98
+ iv = compute_iv_for_decrypting(opt[:salt].b, self[:content])
99
99
 
100
- case confidentiality_mode
101
- when 'gcm'
102
- iv = self[:content].slice!(0, 8)
103
- real_iv = force_binary(opt[:salt]) + iv
104
- when 'cbc'
105
- cipher.padding = 0
106
- real_iv = iv = self[:content].slice!(0, 16)
107
- when 'ctr'
108
- iv = self[:content].slice!(0, 8)
109
- real_iv = force_binary(opt[:salt]) + iv + [1].pack('N')
110
- else
111
- real_iv = iv = self[:content].slice!(0, 16)
100
+ if authenticated?
101
+ if @icv_length.zero?
102
+ @icv_length = opt[:icv_length].to_i if opt[:icv_length]
103
+ raise PacketGen::ParseError, 'unknown ICV size' if @icv_length.zero?
112
104
  end
113
- cipher.iv = real_iv
105
+ icv = self[:content].slice!(-@icv_length, @icv_length)
106
+ end
114
107
 
115
- if authenticated?
116
- if @icv_length.zero?
117
- @icv_length = opt[:icv_length].to_i if opt[:icv_length]
118
- raise ParseError, 'unknown ICV size' if @icv_length.zero?
119
- end
120
- icv = self[:content].slice!(-@icv_length, @icv_length)
121
- end
108
+ authenticate_if_needed iv, icv
109
+ private_decrypt opt
110
+ end
122
111
 
123
- authenticate_if_needed iv, icv
124
- private_decrypt opt
125
- end
112
+ # Encrypt in-place SK payload.
113
+ # @param [OpenSSL::Cipher] cipher keyed cipher
114
+ # This cipher is confidentiality-only one, or AEAD one. To use a second
115
+ # cipher to add integrity, use +:intmode+ option.
116
+ # @param [String] iv IV to encipher SK payload content
117
+ # * CTR and GCM modes: +iv+ is 8-bytes long.
118
+ # @param [Hash] options
119
+ # @option options [Fixnum] :icv_length ICV length for captured packets,
120
+ # or read from PCapNG files
121
+ # @option options [String] :salt salt value for CTR and GCM modes
122
+ # @option options [Fixnum] :pad_length set a padding length
123
+ # @option options [String] :padding set a padding. No check with
124
+ # +:pad_length+ is made. If +:pad_length+ is not set, +:padding+
125
+ # length is shortened to correct padding length
126
+ # @option options [OpenSSL::HMAC] :intmode integrity mode to use with a
127
+ # confidentiality-only cipher. Only HMAC are supported.
128
+ # @return [self]
129
+ def encrypt!(cipher, iv, options={}) # rubocop:disable Naming/MethodParameterName
130
+ opt = { salt: '' }.merge!(options)
126
131
 
127
- # Encrypt in-place SK payload.
128
- # @param [OpenSSL::Cipher] cipher keyed cipher
129
- # This cipher is confidentiality-only one, or AEAD one. To use a second
130
- # cipher to add integrity, use +:intmode+ option.
131
- # @param [String] iv IV to encipher SK payload content
132
- # * CTR and GCM modes: +iv+ is 8-bytes long.
133
- # @param [Hash] options
134
- # @option options [Fixnum] :icv_length ICV length for captured packets,
135
- # or read from PCapNG files
136
- # @option options [String] :salt salt value for CTR and GCM modes
137
- # @option options [Fixnum] :pad_length set a padding length
138
- # @option options [String] :padding set a padding. No check with
139
- # +:pad_length+ is made. If +:pad_length+ is not set, +:padding+
140
- # length is shortened to correct padding length
141
- # @option options [OpenSSL::HMAC] :intmode integrity mode to use with a
142
- # confidentiality-only cipher. Only HMAC are supported.
143
- # @return [self]
144
- def encrypt!(cipher, iv, options={})
145
- opt = { salt: '' }.merge!(options)
132
+ set_crypto cipher, opt[:intmode]
133
+ compute_iv_for_encrypting iv, opt[:salt]
146
134
 
147
- set_crypto cipher, opt[:intmode]
135
+ authenticate_if_needed iv
136
+ encrypted_msg = encrypt_body(iv, opt)
137
+ encrypted_msg << generate_auth_tag(opt) if authenticated?
138
+ self[:content].read(iv + encrypted_msg)
148
139
 
149
- real_iv = force_binary(opt[:salt]) + force_binary(iv)
150
- real_iv += [1].pack('N') if confidentiality_mode == 'ctr'
151
- cipher.iv = real_iv
140
+ # Remove plain payloads
141
+ self[:body] = BinStruct::String.new
152
142
 
153
- authenticate_if_needed iv
143
+ remove_enciphered_packets
144
+ self.calc_length
145
+ self
146
+ end
154
147
 
155
- if opt[:pad_length]
156
- pad_length = opt[:pad_length]
157
- padding = force_binary(opt[:padding] || ([0] * pad_length).pack('C*'))
158
- else
159
- pad_length = cipher.block_size
160
- pad_length = 16 if cipher.block_size == 1 # Some AES mode returns 1...
161
- pad_length -= (self[:body].sz + iv.size + 1) % cipher.block_size
162
- pad_length = 0 if pad_length == 16
163
- padding = force_binary(opt[:padding] || ([0] * pad_length).pack('C*'))
164
- padding = padding[0, pad_length]
165
- end
166
- msg = self[:body].to_s + padding + PacketGen::Types::Int8.new(pad_length).to_s
167
- encrypted_msg = encipher(msg)
168
- cipher.final # message is already padded. No need for mode padding
148
+ private
169
149
 
170
- if authenticated?
171
- @icv_length = opt[:icv_length] if opt[:icv_length]
172
- encrypted_msg << if @conf.authenticated?
173
- @conf.auth_tag[0, @icv_length]
174
- else
175
- @intg.digest[0, @icv_length]
176
- end
177
- end
178
- self[:content].read(iv + encrypted_msg)
150
+ def authenticate_if_needed(iv, icv=nil) # rubocop:disable Naming/MethodParameterName
151
+ if @conf.authenticated?
152
+ @conf.auth_tag = icv if icv
153
+ @conf.auth_data = get_ad
154
+ elsif @intg
155
+ @intg.reset
156
+ @intg.update get_ad
157
+ @intg.update iv
158
+ @icv = icv
159
+ else
160
+ @icv = nil
161
+ end
162
+ end
179
163
 
180
- # Remove plain payloads
181
- self[:body] = PacketGen::Types::String.new
164
+ def encrypt_body(iv, opt) # rubocop:disable Naming/MethodParameterName
165
+ padding, pad_length = compute_padding(iv, opt)
166
+ msg = self[:body].to_s + padding.b + BinStruct::Int8.new(value: pad_length).to_s
167
+ encrypted_msg = encipher(msg)
168
+ @conf.final # message is already padded. No need for mode padding
169
+ encrypted_msg
170
+ end
182
171
 
183
- # Remove enciphered payloads from packet
184
- id = header_id(self)
185
- if id < packet.headers.size - 1
186
- (packet.headers.size - 1).downto(id + 1) do |index|
187
- packet.headers.delete_at index
188
- end
189
- end
172
+ def compute_padding(iv, opt) # rubocop:disable Naming/MethodParameterName
173
+ if opt[:pad_length]
174
+ pad_length = opt[:pad_length]
175
+ padding = opt[:padding] || ([0] * pad_length).pack('C*')
176
+ else
177
+ pad_length = compute_pad_length(iv)
178
+ padding = opt[:padding] || ([0] * pad_length).pack('C*')
179
+ padding = padding[0, pad_length]
180
+ end
181
+ [padding, pad_length]
182
+ end
190
183
 
191
- self.calc_length
192
- self
184
+ def compute_pad_length(iv) # rubocop:disable Naming/MethodParameterName
185
+ pad_length = @conf.block_size
186
+ pad_length = 16 if @conf.block_size == 1 # Some AES mode returns 1...
187
+ pad_length -= (self[:body].sz + iv.size + 1) % @conf.block_size
188
+ pad_length = 0 if pad_length == 16
189
+ pad_length
190
+ end
191
+
192
+ def generate_auth_tag(opt)
193
+ @icv_length = opt[:icv_length] if opt[:icv_length]
194
+ if @conf.authenticated?
195
+ @conf.auth_tag[0, @icv_length]
196
+ else
197
+ @intg.digest[0, @icv_length]
193
198
  end
199
+ end
194
200
 
195
- private
201
+ def remove_enciphered_packets
202
+ id = header_id(self)
203
+ return if id >= packet.headers.size - 1
196
204
 
197
- def authenticate_if_needed(iv, icv=nil)
198
- if @conf.authenticated?
199
- @conf.auth_tag = icv if icv
200
- @conf.auth_data = get_ad
201
- elsif @intg
202
- @intg.reset
203
- @intg.update get_ad
204
- @intg.update iv
205
- @icv = icv
206
- else
207
- @icv = nil
208
- end
205
+ (packet.headers.size - 1).downto(id + 1) do |index|
206
+ packet.headers.delete_at index
209
207
  end
208
+ end
210
209
 
211
- # From RFC 7206, §5.1: The associated data MUST consist of the partial
212
- # contents of the IKEv2 message, starting from the first octet of the
213
- # Fixed IKE Plugin through the last octet of the Payload Plugin of the
214
- # Encrypted Payload (i.e., the fourth octet of the Encrypted Payload).
215
- def get_ad
216
- str = packet.ike.to_s[0, IKE.new.sz]
217
- current_payload = packet.ike[:body]
218
- until current_payload.is_a? SK
219
- str << current_payload.to_s[0, current_payload.to_s.length]
220
- current_payload = current_payload[:body]
221
- end
222
- str << self.to_s[0, SK.new.sz]
210
+ # From RFC 7206, §5.1: The associated data MUST consist of the partial
211
+ # contents of the IKEv2 message, starting from the first octet of the
212
+ # Fixed IKE Plugin through the last octet of the Payload Plugin of the
213
+ # Encrypted Payload (i.e., the fourth octet of the Encrypted Payload).
214
+ def get_ad # rubocop:disable Naming/AccessorMethodName
215
+ str = packet.ike.to_s[0, IKE.new.sz]
216
+ current_payload = packet.ike[:body]
217
+ until current_payload.is_a? SK
218
+ str << current_payload.to_s
219
+ current_payload = current_payload[:body]
223
220
  end
221
+ str << self.to_s[0, SK.new.sz]
222
+ end
224
223
 
225
- def private_decrypt(options)
226
- # decrypt
227
- plain_msg = decipher(content.to_s)
228
- # Remove cipher text
229
- self[:content].read ''
224
+ def private_decrypt(options)
225
+ plain_msg = decipher(content.to_s)
226
+ # Remove cipher text
227
+ self[:content].read ''
230
228
 
231
- # check authentication tag
232
- if authenticated?
233
- return false unless authenticate!
234
- end
229
+ # check authentication tag
230
+ return false if authenticated? && !authenticate!
235
231
 
236
- # remove padding
237
- pad_len = PacketGen::Types::Int8.new.read(plain_msg[-1]).to_i
238
- payloads = plain_msg[0, plain_msg.size - 1 - pad_len]
232
+ payloads = remove_padding(plain_msg)
233
+ if options[:parse]
234
+ parse_ike_payloads(payloads)
235
+ else
236
+ self[:body].read payloads
237
+ end
239
238
 
240
- # parse IKE payloads
241
- if options[:parse]
242
- klass = IKE.constants.select do |c|
243
- cst = IKE.const_get(c)
244
- cst.is_a?(Class) && (cst < Payload) && (cst::PAYLOAD_TYPE == self.next)
245
- end
246
- klass = klass.nil? ? Payload : IKE.const_get(klass.first)
247
- firsth = klass.protocol_name
248
- pkt = Packet.parse(payloads, first_header: firsth)
249
- packet.encapsulate(pkt, parsing: true) unless pkt.nil?
250
- else
251
- self[:body].read payloads
252
- end
239
+ true
240
+ end
253
241
 
254
- true
242
+ def remove_padding(msg)
243
+ pad_len = BinStruct::Int8.new.read(msg[-1]).to_i
244
+ msg[0, msg.size - 1 - pad_len]
245
+ end
246
+
247
+ def parse_ike_payloads(payloads)
248
+ klass = IKE.constants.select do |c|
249
+ cst = IKE.const_get(c)
250
+ cst.is_a?(Class) && (cst < Payload) && (self.next == cst::PAYLOAD_TYPE)
255
251
  end
252
+ klass = klass.nil? ? Payload : IKE.const_get(klass.first)
253
+ firsth = klass.protocol_name
254
+ pkt = PacketGen::Packet.parse(payloads, first_header: firsth)
255
+ packet.encapsulate(pkt, parsing: true) unless pkt.nil?
256
256
  end
257
257
  end
258
-
259
- Header.add_class IKE::SK
260
258
  end
259
+
260
+ PacketGen::Header.add_class IKE::SK
261
261
  end