packetgen-plugin-ipsec 1.0.0

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