packetgen-plugin-ipsec 1.0.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.
@@ -0,0 +1,241 @@
1
+ # This file is part of IPsec packetgen plugin.
2
+ # See https://github.com/sdaubert/packetgen-plugin-ipsec for more informations
3
+ # Copyright (c) 2018 Sylvain Daubert <sylvain.daubert@laposte.net>
4
+ # This program is published under MIT license.
5
+
6
+ # frozen_string_literal: true
7
+
8
+ module PacketGen
9
+ module Plugin
10
+ # This class handles a pseudo-Plugin used to differentiate ESP from IKE Plugins
11
+ # in a UDP datagram with port 4500.
12
+ # @author Sylvain Daubert
13
+ class NonESPMarker < PacketGen::Header::Base
14
+ # @!attribute non_esp_marker
15
+ # 32-bit zero marker to differentiate IKE packet over UDP port 4500 from ESP ones
16
+ # @return [Integer]
17
+ define_field :non_esp_marker, PacketGen::Types::Int32, default: 0
18
+ # @!attribute body
19
+ # @return [PacketGen::Types::String,PacketGen::Header::Base]
20
+ define_field :body, PacketGen::Types::String
21
+
22
+ # Check non_esp_marker field
23
+ # @see [PacketGen::Header::Base#parse?]
24
+ def parse?
25
+ non_esp_marker.zero?
26
+ end
27
+ end
28
+
29
+ # IKE is the Internet Key Exchange protocol (RFC 7296). Ony IKEv2 is supported.
30
+ #
31
+ # A IKE Plugin consists of a Plugin, and a set of payloads. This class
32
+ # handles IKE Plugin. For payloads, see {IKE::Payload}.
33
+ #
34
+ # == IKE Plugin
35
+ # The format of a IKE Plugin is shown below:
36
+ # 1 2 3
37
+ # 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
38
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
39
+ # | IKE SA Initiator's SPI |
40
+ # | |
41
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
42
+ # | IKE SA Responder's SPI |
43
+ # | |
44
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
45
+ # | Next Payload | MjVer | MnVer | Exchange Type | Flags |
46
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
47
+ # | Message ID |
48
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
49
+ # | Length |
50
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
51
+ # A IKE Plugin consists of:
52
+ # * a IKE SA initiator SPI ({#init_spi}, {PacketGen::Types::Int64} type),
53
+ # * a IKE SA responder SPI ({#resp_spi}, {PacketGen::Types::Int64} type),
54
+ # * a Next Payload field ({#next}, {PacketGen::Types::Int8} type),
55
+ # * a Version field ({#version}, {PacketGen::Types::Int8} type, with first 4-bit field
56
+ # as major number, and last 4-bit field as minor number),
57
+ # * a Exchange type ({#exchange_type}, {PacketGen::Types::Int8} type),
58
+ # * a {#flags} field ({PacketGen::Types::Int8} type),
59
+ # * a Message ID ({#message_id}, {PacketGen::Types::Int32} type),
60
+ # * and a {#length} ({PacketGen::Types::Int32} type).
61
+ #
62
+ # == Create a IKE Plugin
63
+ # === Standalone
64
+ # ike = PacketGen::Plugin::IKE.new
65
+ # === Classical IKE packet
66
+ # pkt = PacketGen.gen('IP').add('UDP').add('IKE')
67
+ # # access to IKE Plugin
68
+ # pkt.ike # => PacketGen::Plugin::IKE
69
+ # === NAT-T IKE packet
70
+ # # NonESPMarker is used to insert a 32-bit null field between UDP Plugin
71
+ # # and IKE one to differentiate it from ESP-in-UDP (see RFC 3948)
72
+ # pkt = PacketGen.gen('IP').add('UDP').add('NonESPMarker').add('IKE)
73
+ # @author Sylvain Daubert
74
+ class IKE < PacketGen::Header::Base
75
+ # Classical well-known UDP port for IKE
76
+ UDP_PORT1 = 500
77
+ # Well-known UDP port for IKE when NAT is detected
78
+ UDP_PORT2 = 4500
79
+
80
+ PROTOCOLS = {
81
+ 'IKE' => 1,
82
+ 'AH' => 2,
83
+ 'ESP' => 3
84
+ }.freeze
85
+
86
+ EXCHANGE_TYPES = {
87
+ 'IKE_SA_INIT' => 34,
88
+ 'IKE_AUTH' => 35,
89
+ 'CREATE_CHILD_SA' => 36,
90
+ 'INFORMATIONAL' => 37
91
+ }.freeze
92
+
93
+ # @!attribute init_spi
94
+ # 64-bit initiator SPI
95
+ # @return [Integer]
96
+ define_field :init_spi, PacketGen::Types::Int64
97
+ # @!attribute resp_spi
98
+ # 64-bit responder SPI
99
+ # @return [Integer]
100
+ define_field :resp_spi, PacketGen::Types::Int64
101
+ # @!attribute next
102
+ # 8-bit next payload type
103
+ # @return [Integer]
104
+ define_field :next, PacketGen::Types::Int8
105
+ # @!attribute version
106
+ # 8-bit IKE version
107
+ # @return [Integer]
108
+ define_field :version, PacketGen::Types::Int8, default: 0x20
109
+ # @!attribute [r] exchange_type
110
+ # 8-bit exchange type
111
+ # @return [Integer]
112
+ define_field :exchange_type, PacketGen::Types::Int8Enum, enum: EXCHANGE_TYPES
113
+ # @!attribute flags
114
+ # 8-bit flags
115
+ # @return [Integer]
116
+ define_field :flags, PacketGen::Types::Int8
117
+ # @!attribute message_id
118
+ # 32-bit message ID
119
+ # @return [Integer]
120
+ define_field :message_id, PacketGen::Types::Int32
121
+ # @!attribute length
122
+ # 32-bit length of total message (Plugin + payloads)
123
+ # @return [Integer]
124
+ define_field :length, PacketGen::Types::Int32
125
+
126
+ # Defining a body permits using Packet#parse to parse IKE payloads.
127
+ # But this method is hidden as prefered way to access payloads is via #payloads
128
+ define_field :body, PacketGen::Types::String
129
+
130
+ # @!attribute mjver
131
+ # 4-bit major version value
132
+ # @return [Integer]
133
+ # @!attribute mnver
134
+ # 4-bit minor version value
135
+ # @return [Integer]
136
+ define_bit_fields_on :version, :mjver, 4, :mnver, 4
137
+
138
+ # @!attribute rsv1
139
+ # @return [Integer]
140
+ # @!attribute rsv2
141
+ # @return [Integer]
142
+ # @!attribute flag_i
143
+ # bit set in message sent by the original initiator
144
+ # @return [Boolean]
145
+ # @!attribute flag_r
146
+ # indicate this message is a response to a message containing the same Message ID
147
+ # @return [Boolean]
148
+ # @!attribute flag_v
149
+ # version flag. Ignored by IKEv2 peers, and should be set to 0
150
+ # @return [Boolean]
151
+ define_bit_fields_on :flags, :rsv1, 2, :flag_r, :flag_v, :flag_i, :rsv2, 3
152
+
153
+ # @param [Hash] options
154
+ # @see PacketGen::Header::Base#initialize
155
+ def initialize(options={})
156
+ super
157
+ calc_length unless options[:length]
158
+ self.type = options[:type] if options[:type]
159
+ self.type = options[:exchange_type] if options[:exchange_type]
160
+ end
161
+
162
+ alias type exchange_type
163
+ alias type= exchange_type=
164
+
165
+ # Get exchange type name
166
+ # @return [String
167
+ def human_exchange_type
168
+ self[:exchange_type].to_human
169
+ end
170
+ alias human_type human_exchange_type
171
+
172
+ # Calculate length field
173
+ # @return [Integer]
174
+ def calc_length
175
+ PacketGen::Header::Base.calculate_and_set_length self
176
+ end
177
+
178
+ # IKE payloads
179
+ # @return [Array<Payload>]
180
+ def payloads
181
+ payloads = []
182
+ body = self.body
183
+ while body.is_a?(Payload)
184
+ payloads << body
185
+ body = body.body
186
+ end
187
+ payloads
188
+ end
189
+
190
+ # @return [String]
191
+ def inspect
192
+ super do |attr|
193
+ case attr
194
+ when :flags
195
+ str_flags = ''.dup
196
+ %w[r v i].each do |flag|
197
+ str_flags << (send("flag_#{flag}?") ? flag.upcase : '.')
198
+ end
199
+ str = Inspect.shift_level
200
+ str << Inspect::FMT_ATTR % [self[attr].class.to_s.sub(/.*::/, ''), attr,
201
+ str_flags]
202
+ end
203
+ end
204
+ end
205
+
206
+ # Toggle +I+ and +R+ flags.
207
+ # @return [self]
208
+ def reply!
209
+ self.flag_r = !self.flag_r?
210
+ self.flag_i = !self.flag_i?
211
+ self
212
+ end
213
+
214
+ # @api private
215
+ # @note This method is used internally by PacketGen and should not be
216
+ # directly called
217
+ # @param [Packet] packet
218
+ # @return [void]
219
+ def added_to_packet(packet)
220
+ return unless packet.is? 'UDP'
221
+ return unless packet.udp.sport.zero?
222
+ packet.udp.sport = if packet.is?('NonESPMarker')
223
+ UDP_PORT2
224
+ else
225
+ UDP_PORT1
226
+ end
227
+ end
228
+ end
229
+
230
+ Header.add_class IKE
231
+ Header.add_class NonESPMarker
232
+
233
+ PacketGen::Header::UDP.bind IKE, dport: IKE::UDP_PORT1
234
+ PacketGen::Header::UDP.bind IKE, sport: IKE::UDP_PORT1
235
+ PacketGen::Header::UDP.bind NonESPMarker, dport: IKE::UDP_PORT2
236
+ PacketGen::Header::UDP.bind NonESPMarker, sport: IKE::UDP_PORT2
237
+ NonESPMarker.bind IKE
238
+ end
239
+ end
240
+
241
+ require_relative 'ike/payload'
@@ -0,0 +1,165 @@
1
+ # coding: utf-8
2
+ # This file is part of IPsec packetgen plugin.
3
+ # See https://github.com/sdaubert/packetgen-plugin-ipsec for more informations
4
+ # Copyright (c) 2018 Sylvain Daubert <sylvain.daubert@laposte.net>
5
+ # This program is published under MIT license.
6
+
7
+ # frozen_string_literal: true
8
+
9
+ module PacketGen
10
+ module Plugin
11
+ class IKE
12
+ # This class handles Authentication payloads.
13
+ #
14
+ # A AUTH payload consists of the IKE generic payload Plugin (see {Payload})
15
+ # and some specific fields:
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
+ # | Auth Method | RESERVED |
22
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
23
+ # | |
24
+ # ~ Authentication Data ~
25
+ # | |
26
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
27
+ # These specific fields are:
28
+ # * {#type} (ID type),
29
+ # * {#reserved},
30
+ # * and {#content} (Identification Data).
31
+ #
32
+ # == Create a KE payload
33
+ # # create a IKE packet with a Auth payload
34
+ # pkt = PacketGen.gen('IP').add('UDP').add('IKE').add('IKE::Auth', method: 'SHARED_KEY')
35
+ # pkt.calc_length
36
+ # @author Sylvain Daubert
37
+ class Auth < Payload
38
+ # Payload type number
39
+ PAYLOAD_TYPE = 39
40
+
41
+ METHODS = {
42
+ 'RSA_SIGNATURE' => 1,
43
+ 'SHARED_KEY' => 2,
44
+ 'DSA_SIGNATURE' => 3,
45
+ 'ECDSA256' => 9,
46
+ 'ECDSA384' => 10,
47
+ 'ECDSA512' => 11,
48
+ 'PASSWORD' => 12,
49
+ 'NULL' => 13,
50
+ 'DIGITAL_SIGNATURE' => 14
51
+ }.freeze
52
+
53
+ # @attribute [r] method
54
+ # 8-bit Auth Method
55
+ # @return [Integer]
56
+ define_field_before :content, :method, PacketGen::Types::Int8Enum, enum: METHODS
57
+ # @attribute reserved
58
+ # 24-bit reserved field
59
+ # @return [Integer]
60
+ define_field_before :content, :reserved, PacketGen::Types::Int24
61
+
62
+ # Check authentication (see RFC 7296 §2.15)
63
+ # @param [Packet] init_msg first IKE message sent by peer
64
+ # @param [String] nonce my nonce, sent in first message
65
+ # @param [String] sk_p secret key used to compute prf(SK_px, IDx')
66
+ # @param [Integer] prf PRF type to use (see {Transform}+::PRF_*+ constants)
67
+ # @param [String] shared_secret shared secret to use as PSK (shared secret
68
+ # method only)
69
+ # @param [OpenSSL::X509::Certificate] cert certificate to check AUTH signature,
70
+ # if not embedded in IKE message
71
+ # @return [Boolean]
72
+ # @note For now, only NULL, SHARED_KEY and RSA, DSA and ECDSA signatures are
73
+ # supported.
74
+ # @note For certificates, only check AUTH authenticity with given (or guessed
75
+ # from packet) certificate, but certificate chain is not verified.
76
+ def check?(init_msg: nil, nonce: '', sk_p: '', prf: 1, shared_secret: '',
77
+ cert: nil)
78
+ raise TypeError, 'init_msg should be a Packet' unless init_msg.is_a?(Packet)
79
+ signed_octets = init_msg.ike.to_s
80
+ signed_octets << nonce
81
+ id = packet.ike.flag_i? ? packet.ike_idi : packet.ike_idr
82
+ signed_octets << prf(prf, sk_p, id.to_s[4, id.length - 4])
83
+
84
+ case method
85
+ when METHODS['SHARED_KEY']
86
+ auth = prf(prf(shared_secret, 'Key Pad for IKEv2'), signed_octets)
87
+ auth == content
88
+ when METHODS['RSA_SIGNATURE'], METHODS['ECDSA256'], METHODS['ECDSA384'],
89
+ METHODS['ECDSA512']
90
+ if packet.ike_cert
91
+ # FIXME: Expect a ENCODING_X509_CERT_SIG
92
+ # Others types not supported for now...
93
+ cert = OpenSSL::X509::Certificate.new(packet.ike_cert.content)
94
+ elsif cert.nil?
95
+ raise CryptoError, 'a certificate should be provided'
96
+ end
97
+
98
+ text = cert.to_text
99
+ m = text.match(/Public Key Algorithm: ([a-zA-Z0-9-]+)/)
100
+ digest = case m[1]
101
+ when 'id-ecPublicKey'
102
+ m2 = text.match(/Public-Key: \((\d+) bit\)/)
103
+ case m2[1]
104
+ when '256'
105
+ OpenSSL::Digest::SHA256.new
106
+ when '384'
107
+ OpenSSL::Digest::SHA384.new
108
+ when '521'
109
+ OpenSSL::Digest::SHA512.new
110
+ end
111
+ when /sha([235]\d+)/
112
+ OpenSSL::Digest.const_get("SHA#{$1}").new
113
+ when /sha1/, 'rsaEncryption'
114
+ OpenSSL::Digest::SHA1.new
115
+ end
116
+ signature = format_signature(cert.public_key, content.to_s)
117
+ cert.public_key.verify(digest, signature, signed_octets)
118
+ when METHOD_NULL
119
+ true
120
+ else
121
+ raise NotImplementedError, "unsupported method #{human_method}"
122
+ end
123
+ end
124
+
125
+ # Get authentication method name
126
+ # @return [String]
127
+ def human_method
128
+ self[:method].to_human
129
+ end
130
+
131
+ private
132
+
133
+ def prf(type, key, msg)
134
+ case type
135
+ when Transform::PRF_HMAC_MD5, Transform::PRF_HMAC_SHA1,
136
+ Transform::PRF_HMAC_SHA2_256, Transform::PRF_HMAC_SHA2_384,
137
+ Transform::PRF_HMAC_SHA2_512
138
+ digestname = Transform.constants.grep(/PRF_/)
139
+ .detect { |c| Transform.const_get(c) == type }
140
+ .to_s.sub(/^PRF_HMAC_/, '').sub(/2_/, '')
141
+ digest = OpenSSL::Digest.const_get(digestname).new
142
+ else
143
+ raise NotImplementedError, 'for now, only HMAC-based PRF are supported'
144
+ end
145
+ hmac = OpenSSL::HMAC.new(key, digest)
146
+ hmac << msg
147
+ hmac.digest
148
+ end
149
+
150
+ def format_signature(pkey, sig)
151
+ if pkey.is_a?(OpenSSL::PKey::EC)
152
+ # PKey::EC need a signature as a DER string representing a sequence of
153
+ # 2 integers: r and s
154
+ r = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(sig[0, sig.size / 2], 2).to_i)
155
+ s = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(sig[sig.size / 2,
156
+ sig.size / 2], 2).to_i)
157
+ OpenSSL::ASN1::Sequence.new([r, s]).to_der
158
+ else
159
+ sig
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,76 @@
1
+ # coding: utf-8
2
+ # This file is part of IPsec packetgen plugin.
3
+ # See https://github.com/sdaubert/packetgen-plugin-ipsec for more informations
4
+ # Copyright (c) 2018 Sylvain Daubert <sylvain.daubert@laposte.net>
5
+ # This program is published under MIT license.
6
+
7
+ # frozen_string_literal: true
8
+
9
+ module PacketGen
10
+ module Plugin
11
+ class IKE
12
+ # This class handles Certificate payloads.
13
+ #
14
+ # A Cert payload consists of the IKE generic payload Plugin (see {Payload})
15
+ # and some specific fields:
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
+ # | Cert Encoding | |
22
+ # +-+-+-+-+-+-+-+-+ +
23
+ # | |
24
+ # ~ Certificate Data ~
25
+ # | |
26
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
27
+ # These specific fields are:
28
+ # * {#encoding},
29
+ # * and {#content} (Certificate Data).
30
+ #
31
+ # == Create a Cert payload
32
+ # # Create a IKE packet with a Cert payload
33
+ # pkt = PacketGen.gen('IP').add('UDP').add('IKE').add('IKE::Cert', encoding: 'X509_CERT_SIG')
34
+ # certs = cert.to_der << ca_cert.to_der
35
+ # pkt.ike_cert.content.read certs
36
+ # pkt.calc_length
37
+ # @author Sylvain Daubert
38
+ class Cert < Payload
39
+ # Payload type number
40
+ PAYLOAD_TYPE = 37
41
+
42
+ ENCODINGS = {
43
+ 'PKCS7_WRAPPED_X509' => 1,
44
+ 'PGP' => 2,
45
+ 'DNS_SIGNED_KEY' => 3,
46
+ 'X509_CERT_SIG' => 4,
47
+ 'KERBEROS_TOKEN' => 6,
48
+ 'X509_CRL' => 7,
49
+ 'X509_ARL' => 8,
50
+ 'SPKI_CERT' => 9,
51
+ 'X509_CERT_ATTR' => 10,
52
+ 'HASH_URL_X509_CERT' => 12,
53
+ 'HASH_URL_X509_BUNDLE' => 13
54
+ }.freeze
55
+
56
+ # @attribute encoding
57
+ # 8-bit certificate encoding
58
+ # @return [Integer]
59
+ define_field_before :content, :encoding, PacketGen::Types::Int8Enum, enum: ENCODINGS
60
+
61
+ def initialize(options={})
62
+ super
63
+ self.encoding = options[:encoding] if options[:encoding]
64
+ end
65
+
66
+ # Get encoding name
67
+ # @return [String]
68
+ def human_encoding
69
+ self[:encoding].to_human
70
+ end
71
+ end
72
+ end
73
+
74
+ Header.add_class IKE::Cert
75
+ end
76
+ end