1# SPDX-License-Identifier: GPL-2.0-only
2# This file is part of Scapy
3# See https://scapy.net/ for more information
4# Copyright (C) Philippe Biondi <phil@secdev.org>
5
6"""
7ISAKMP (Internet Security Association and Key Management Protocol).
8"""
9
10# Mostly based on https://tools.ietf.org/html/rfc2408
11
12import struct
13from scapy.config import conf
14from scapy.packet import Packet, bind_bottom_up, bind_top_down, bind_layers
15from scapy.compat import chb
16from scapy.fields import (
17 ByteEnumField,
18 ByteField,
19 FieldLenField,
20 FieldListField,
21 FlagsField,
22 IPField,
23 IntEnumField,
24 IntField,
25 MultipleTypeField,
26 PacketLenField,
27 ShortEnumField,
28 ShortField,
29 StrLenEnumField,
30 StrLenField,
31 XByteField,
32 XStrFixedLenField,
33 XStrLenField,
34)
35from scapy.layers.inet import IP, UDP
36from scapy.layers.ipsec import NON_ESP
37from scapy.sendrecv import sr
38from scapy.volatile import RandString
39from scapy.error import warning
40from functools import reduce
41
42# TODO: some ISAKMP payloads are not implemented,
43# and inherit a default ISAKMP_payload
44
45
46# see https://www.iana.org/assignments/ipsec-registry/ipsec-registry.xhtml#ipsec-registry-2 for details # noqa: E501
47ISAKMPAttributeTypes = {
48 "Encryption": (1, {"DES-CBC": 1,
49 "IDEA-CBC": 2,
50 "Blowfish-CBC": 3,
51 "RC5-R16-B64-CBC": 4,
52 "3DES-CBC": 5,
53 "CAST-CBC": 6,
54 "AES-CBC": 7,
55 "CAMELLIA-CBC": 8, }, 0),
56 "Hash": (2, {"MD5": 1,
57 "SHA": 2,
58 "Tiger": 3,
59 "SHA2-256": 4,
60 "SHA2-384": 5,
61 "SHA2-512": 6, }, 0),
62 "Authentication": (3, {"PSK": 1,
63 "DSS": 2,
64 "RSA Sig": 3,
65 "RSA Encryption": 4,
66 "RSA Encryption Revised": 5,
67 "ElGamal Encryption": 6,
68 "ElGamal Encryption Revised": 7,
69 "ECDSA Sig": 8,
70 "HybridInitRSA": 64221,
71 "HybridRespRSA": 64222,
72 "HybridInitDSS": 64223,
73 "HybridRespDSS": 64224,
74 "XAUTHInitPreShared": 65001,
75 "XAUTHRespPreShared": 65002,
76 "XAUTHInitDSS": 65003,
77 "XAUTHRespDSS": 65004,
78 "XAUTHInitRSA": 65005,
79 "XAUTHRespRSA": 65006,
80 "XAUTHInitRSAEncryption": 65007,
81 "XAUTHRespRSAEncryption": 65008,
82 "XAUTHInitRSARevisedEncryption": 65009, # noqa: E501
83 "XAUTHRespRSARevisedEncryptio": 65010, }, 0), # noqa: E501
84 "GroupDesc": (4, {"768MODPgr": 1,
85 "1024MODPgr": 2,
86 "EC2Ngr155": 3,
87 "EC2Ngr185": 4,
88 "1536MODPgr": 5,
89 "2048MODPgr": 14,
90 "3072MODPgr": 15,
91 "4096MODPgr": 16,
92 "6144MODPgr": 17,
93 "8192MODPgr": 18, }, 0),
94 "GroupType": (5, {"MODP": 1,
95 "ECP": 2,
96 "EC2N": 3}, 0),
97 "GroupPrime": (6, {}, 1),
98 "GroupGenerator1": (7, {}, 1),
99 "GroupGenerator2": (8, {}, 1),
100 "GroupCurveA": (9, {}, 1),
101 "GroupCurveB": (10, {}, 1),
102 "LifeType": (11, {"Seconds": 1,
103 "Kilobytes": 2}, 0),
104 "LifeDuration": (12, {}, 1),
105 "PRF": (13, {}, 0),
106 "KeyLength": (14, {}, 0),
107 "FieldSize": (15, {}, 0),
108 "GroupOrder": (16, {}, 1),
109}
110
111# see https://www.iana.org/assignments/isakmp-registry/isakmp-registry.xhtml#isakmp-registry-13 for details # noqa: E501
112IPSECAttributeTypes = {
113 "LifeType": (1, {"Reserved": 0,
114 "seconds": 1,
115 "kilobytes": 2}, 0),
116 "LifeDuration": (2, {}, 1),
117 "GroupDesc": (3, ISAKMPAttributeTypes["GroupDesc"][1], 0),
118 "EncapsulationMode": (4, {"Reserved": 0,
119 "Tunnel": 1,
120 "Transport": 2,
121 "UDP-Encapsulated-Tunnel": 3,
122 "UDP-Encapsulated-Transport": 4}, 0),
123 "AuthenticationAlgorithm": (5, {"HMAC-MD5": 1,
124 "HMAC-SHA": 2,
125 "DES-MAC": 3,
126 "KPDK": 4,
127 "HMAC-SHA2-256": 5,
128 "HMAC-SHA2-384": 6,
129 "HMAC-SHA2-512": 7,
130 "HMAC-RIPEMD": 8,
131 "AES-XCBC-MAC": 9,
132 "SIG-RSA": 10,
133 "AES-128-GMAC": 11,
134 "AES-192-GMAC": 12,
135 "AES-256-GMAC": 13}, 0),
136 "KeyLength": (6, {}, 0),
137 "KeyRounds": (7, {}, 0),
138 "CompressDictionarySize": (8, {}, 0),
139 "CompressPrivateAlgorithm": (9, {}, 1),
140}
141
142_rev = lambda x: {
143 v[0]: (k, {vv: kk for kk, vv in v[1].items()}, v[2])
144 for k, v in x.items()
145}
146ISAKMPTransformNum = _rev(ISAKMPAttributeTypes)
147IPSECTransformNum = _rev(IPSECAttributeTypes)
148
149# See IPSEC Security Protocol Identifiers entry in
150# https://www.iana.org/assignments/isakmp-registry/isakmp-registry.xhtml#isakmp-registry-3
151PROTO_ISAKMP = 1
152PROTO_IPSEC_AH = 2
153PROTO_IPSEC_ESP = 3
154PROTO_IPCOMP = 4
155PROTO_GIGABEAM_RADIO = 5
156
157
158class ISAKMPTransformSetField(StrLenField):
159 islist = 1
160
161 @staticmethod
162 def type2num(type_val_tuple, proto=0):
163 typ, val = type_val_tuple
164 if proto == PROTO_ISAKMP:
165 type_val, enc_dict, tlv = ISAKMPAttributeTypes.get(typ, (typ, {}, 0))
166 elif proto == PROTO_IPSEC_ESP:
167 type_val, enc_dict, tlv = IPSECAttributeTypes.get(typ, (typ, {}, 0))
168 else:
169 type_val, enc_dict, tlv = (typ, {}, 0)
170 val = enc_dict.get(val, val)
171 if isinstance(val, str):
172 raise ValueError("Unknown attribute '%s'" % val)
173 s = b""
174 if (val & ~0xffff):
175 if not tlv:
176 warning("%r should not be TLV but is too big => using TLV encoding" % typ) # noqa: E501
177 n = 0
178 while val:
179 s = chb(val & 0xff) + s
180 val >>= 8
181 n += 1
182 val = n
183 else:
184 type_val |= 0x8000
185 return struct.pack("!HH", type_val, val) + s
186
187 @staticmethod
188 def num2type(typ, enc, proto=0):
189 if proto == PROTO_ISAKMP:
190 val = ISAKMPTransformNum.get(typ, (typ, {}))
191 elif proto == PROTO_IPSEC_ESP:
192 val = IPSECTransformNum.get(typ, (typ, {}))
193 else:
194 val = (typ, {})
195 enc = val[1].get(enc, enc)
196 return (val[0], enc)
197
198 def _get_proto(self, pkt):
199 # Ugh
200 cur = pkt
201 while cur and getattr(cur, "proto", None) is None:
202 cur = cur.parent or cur.underlayer
203 if cur is None:
204 return PROTO_ISAKMP
205 return cur.proto
206
207 def i2m(self, pkt, i):
208 if i is None:
209 return b""
210 proto = self._get_proto(pkt)
211 i = [ISAKMPTransformSetField.type2num(e, proto=proto) for e in i]
212 return b"".join(i)
213
214 def m2i(self, pkt, m):
215 # I try to ensure that we don't read off the end of our packet based
216 # on bad length fields we're provided in the packet. There are still
217 # conditions where struct.unpack() may not get enough packet data, but
218 # worst case that should result in broken attributes (which would
219 # be expected). (wam)
220 lst = []
221 proto = self._get_proto(pkt)
222 while len(m) >= 4:
223 trans_type, = struct.unpack("!H", m[:2])
224 is_tlv = not (trans_type & 0x8000)
225 if is_tlv:
226 # We should probably check to make sure the attribute type we
227 # are looking at is allowed to have a TLV format and issue a
228 # warning if we're given an TLV on a basic attribute.
229 value_len, = struct.unpack("!H", m[2:4])
230 if value_len + 4 > len(m):
231 warning("Bad length for ISAKMP transform type=%#6x" % trans_type) # noqa: E501
232 value = m[4:4 + value_len]
233 value = reduce(lambda x, y: (x << 8) | y, struct.unpack("!%s" % ("B" * len(value),), value), 0) # noqa: E501
234 else:
235 trans_type &= 0x7fff
236 value_len = 0
237 value, = struct.unpack("!H", m[2:4])
238 m = m[4 + value_len:]
239 lst.append(ISAKMPTransformSetField.num2type(trans_type, value, proto=proto))
240 if len(m) > 0:
241 warning("Extra bytes after ISAKMP transform dissection [%r]" % m)
242 return lst
243
244
245ISAKMP_payload_type = {
246 0: "None",
247 1: "SA",
248 2: "Proposal",
249 3: "Transform",
250 4: "KE",
251 5: "ID",
252 6: "CERT",
253 7: "CR",
254 8: "Hash",
255 9: "SIG",
256 10: "Nonce",
257 11: "Notification",
258 12: "Delete",
259 13: "VendorID",
260}
261
262ISAKMP_exchange_type = {
263 0: "None",
264 1: "base",
265 2: "identity protection",
266 3: "authentication only",
267 4: "aggressive",
268 5: "informational",
269 32: "quick mode",
270}
271
272# https://www.iana.org/assignments/isakmp-registry/isakmp-registry.xhtml#isakmp-registry-3
273# IPSEC Security Protocol Identifiers
274ISAKMP_protos = {
275 1: "ISAKMP",
276 2: "IPSEC_AH",
277 3: "IPSEC_ESP",
278 4: "IPCOMP",
279 5: "GIGABEAM_RADIO"
280}
281
282ISAKMP_doi = {
283 0: "ISAKMP",
284 1: "IPSEC",
285}
286
287
288class _ISAKMP_class(Packet):
289 def default_payload_class(self, payload):
290 if self.next_payload == 0:
291 return conf.raw_layer
292 return ISAKMP_payload
293
294# -- ISAKMP
295
296
297class ISAKMP(_ISAKMP_class): # rfc2408
298 name = "ISAKMP"
299 fields_desc = [
300 XStrFixedLenField("init_cookie", "", 8),
301 XStrFixedLenField("resp_cookie", "", 8),
302 ByteEnumField("next_payload", 0, ISAKMP_payload_type),
303 XByteField("version", 0x10),
304 ByteEnumField("exch_type", 0, ISAKMP_exchange_type),
305 FlagsField("flags", 0, 8, ["encryption", "commit", "auth_only"]),
306 IntField("id", 0),
307 IntField("length", None)
308 ]
309
310 def guess_payload_class(self, payload):
311 if self.flags & 1:
312 return conf.raw_layer
313 return _ISAKMP_class.guess_payload_class(self, payload)
314
315 def answers(self, other):
316 if isinstance(other, ISAKMP):
317 if other.init_cookie == self.init_cookie:
318 return 1
319 return 0
320
321 def post_build(self, p, pay):
322 p += pay
323 if self.length is None:
324 p = p[:24] + struct.pack("!I", len(p)) + p[28:]
325 return p
326
327
328# -- ISAKMP payloads
329
330class ISAKMP_payload(_ISAKMP_class):
331 name = "ISAKMP payload"
332 show_indent = 0
333 fields_desc = [
334 ByteEnumField("next_payload", None, ISAKMP_payload_type),
335 ByteField("res", 0),
336 ShortField("length", None),
337 XStrLenField("load", "", length_from=lambda x:x.length - 4),
338 ]
339
340 def post_build(self, pkt, pay):
341 if self.length is None:
342 pkt = pkt[:2] + struct.pack("!H", len(pkt)) + pkt[4:]
343 return pkt + pay
344
345
346class ISAKMP_payload_Transform(ISAKMP_payload):
347 name = "IKE Transform"
348 deprecated_fields = {
349 "num": ("transform_count", ("2.5.0")),
350 "id": ("transform_id", ("2.5.0")),
351 }
352 fields_desc = ISAKMP_payload.fields_desc[:3] + [
353 ByteField("transform_count", None),
354 ByteEnumField("transform_id", 1, {1: "KEY_IKE"}),
355 ShortField("res2", 0),
356 ISAKMPTransformSetField("transforms", None, length_from=lambda x: x.length - 8) # noqa: E501
357 # XIntField("enc",0x80010005L),
358 # XIntField("hash",0x80020002L),
359 # XIntField("auth",0x80030001L),
360 # XIntField("group",0x80040002L),
361 # XIntField("life_type",0x800b0001L),
362 # XIntField("durationh",0x000c0004L),
363 # XIntField("durationl",0x00007080L),
364 ]
365
366
367# https://tools.ietf.org/html/rfc2408#section-3.5
368class ISAKMP_payload_Proposal(ISAKMP_payload):
369 name = "IKE proposal"
370 fields_desc = ISAKMP_payload.fields_desc[:3] + [
371 ByteField("proposal", 1),
372 ByteEnumField("proto", 1, ISAKMP_protos),
373 FieldLenField("SPIsize", None, "SPI", "B"),
374 ByteField("trans_nb", None),
375 StrLenField("SPI", "", length_from=lambda x: x.SPIsize),
376 PacketLenField("trans", conf.raw_layer(), ISAKMP_payload_Transform, length_from=lambda x: x.length - 8), # noqa: E501
377 ]
378
379
380# VendorID: https://www.rfc-editor.org/rfc/rfc2408#section-3.16
381
382# packet-isakmp.c from wireshark
383ISAKMP_VENDOR_IDS = {
384 b"\x09\x00\x26\x89\xdf\xd6\xb7\x12": "XAUTH",
385 b"\xaf\xca\xd7\x13h\xa1\xf1\xc9k\x86\x96\xfcwW\x01\x00": "RFC 3706 DPD",
386 b"@H\xb7\xd5n\xbc\xe8\x85%\xe7\xde\x7f\x00\xd6\xc2\xd3\x80": "Cisco Fragmentation",
387 b"J\x13\x1c\x81\x07\x03XE\\W(\xf2\x0e\x95E/": "RFC 3947 Negotiation of NAT-Transversal", # noqa: E501
388 b"\x90\xcb\x80\x91>\xbbin\x08c\x81\xb5\xecB{\x1f": "draft-ietf-ipsec-nat-t-ike-02",
389}
390
391
392class ISAKMP_payload_VendorID(ISAKMP_payload):
393 name = "ISAKMP Vendor ID"
394 fields_desc = ISAKMP_payload.fields_desc[:3] + [
395 StrLenEnumField("VendorID", b"",
396 ISAKMP_VENDOR_IDS,
397 length_from=lambda x: x.length - 4)
398 ]
399
400
401class ISAKMP_payload_SA(ISAKMP_payload):
402 name = "ISAKMP SA"
403 fields_desc = ISAKMP_payload.fields_desc[:3] + [
404 IntEnumField("doi", 1, ISAKMP_doi),
405 IntEnumField("situation", 1, {1: "identity"}),
406 PacketLenField("prop", conf.raw_layer(), ISAKMP_payload_Proposal, length_from=lambda x: x.length - 12), # noqa: E501
407 ]
408
409
410class ISAKMP_payload_Nonce(ISAKMP_payload):
411 name = "ISAKMP Nonce"
412 deprecated_fields = {"load": ("nonce", "2.6.2")}
413 fields_desc = ISAKMP_payload.fields_desc[:3] + [
414 StrLenField("nonce", "", length_from=lambda x: x.length - 4)
415 ]
416
417
418class ISAKMP_payload_KE(ISAKMP_payload):
419 name = "ISAKMP Key Exchange"
420 deprecated_fields = {"load": ("ke", "2.6.2")}
421 fields_desc = ISAKMP_payload.fields_desc[:3] + [
422 StrLenField("ke", "", length_from=lambda x: x.length - 4)
423 ]
424
425
426class ISAKMP_payload_ID(ISAKMP_payload):
427 name = "ISAKMP Identification"
428 fields_desc = ISAKMP_payload.fields_desc[:3] + [
429 ByteEnumField("IDtype", 1, {
430 # Beware, apparently in-the-wild the values used
431 # appear to be the ones from IKEv2 (RFC4306 sect 3.5)
432 # and not ISAKMP (RFC2408 sect A.4)
433 1: "IPv4_addr",
434 11: "Key"
435 }),
436 ByteEnumField("ProtoID", 0, {0: "Unused"}),
437 ShortEnumField("Port", 0, {0: "Unused"}),
438 MultipleTypeField(
439 [
440 (IPField("IdentData", "127.0.0.1"),
441 lambda pkt: pkt.IDtype == 1),
442 ],
443 StrLenField("IdentData", "", length_from=lambda x: x.length - 8),
444 )
445 ]
446
447
448class ISAKMP_payload_Hash(ISAKMP_payload):
449 name = "ISAKMP Hash"
450 deprecated_fields = {"load": ("hash", "2.6.2")}
451 fields_desc = ISAKMP_payload.fields_desc[:3] + [
452 StrLenField("hash", "", length_from=lambda x: x.length - 4)
453 ]
454
455
456class ISAKMP_payload_SIG(ISAKMP_payload):
457 name = "ISAKMP Signature"
458 deprecated_fields = {"load": ("sig", "2.6.2")}
459 fields_desc = ISAKMP_payload.fields_desc[:3] + [
460 StrLenField("sig", "", length_from=lambda x: x.length - 4)
461 ]
462
463
464NotifyMessageType = {
465 1: "INVALID-PAYLOAD-TYPE",
466 2: "DOI-NOT-SUPPORTED",
467 3: "SITUATION-NOT-SUPPORTED",
468 4: "INVALID-COOKIE",
469 5: "INVALID-MAJOR-VERSION",
470 6: "INVALID-MINOR-VERSION",
471 7: "INVALID-EXCHANGE-TYPE",
472 8: "INVALID-FLAGS",
473 9: "INVALID-MESSAGE-ID",
474 10: "INVALID-PROTOCOL-ID",
475 11: "INVALID-SPI",
476 12: "INVALID-TRANSFORM-ID",
477 13: "ATTRIBUTES-NOT-SUPPORTED",
478 14: "NO-PROPOSAL-CHOSEN",
479 15: "BAD-PROPOSAL-SYNTAX",
480 16: "PAYLOAD-MALFORMED",
481 17: "INVALID-KEY-INFORMATION",
482 18: "INVALID-ID-INFORMATION",
483 19: "INVALID-CERT-ENCODING",
484 20: "INVALID-CERTIFICATE",
485 21: "CERT-TYPE-UNSUPPORTED",
486 22: "INVALID-CERT-AUTHORITY",
487 23: "INVALID-HASH-INFORMATION",
488 24: "AUTHENTICATION-FAILED",
489 25: "INVALID-SIGNATURE",
490 26: "ADDRESS-NOTIFICATION",
491 27: "NOTIFY-SA-LIFETIME",
492 28: "CERTIFICATE-UNAVAILABLE",
493 29: "UNSUPPORTED-EXCHANGE-TYPE",
494 30: "UNEQUAL-PAYLOAD-LENGTHS",
495 16384: "CONNECTED",
496 # RFC 3706
497 36136: "R-U-THERE",
498 36137: "R-U-THERE-ACK",
499}
500
501
502class ISAKMP_payload_Notify(ISAKMP_payload):
503 name = "ISAKMP Notify (Notification)"
504 fields_desc = ISAKMP_payload.fields_desc[:3] + [
505 IntEnumField("doi", 0, ISAKMP_doi),
506 ByteEnumField("proto", 1, ISAKMP_protos),
507 FieldLenField("SPIsize", None, "SPI", "B"),
508 ShortEnumField("notify_msg_type", None, NotifyMessageType),
509 StrLenField("SPI", "", length_from=lambda x: x.SPIsize),
510 StrLenField("notify_data", "",
511 length_from=lambda x: x.length - x.SPIsize - 12)
512 ]
513
514
515class ISAKMP_payload_Delete(ISAKMP_payload):
516 name = "ISAKMP Delete"
517 fields_desc = ISAKMP_payload.fields_desc[:3] + [
518 IntEnumField("doi", 0, ISAKMP_doi),
519 ByteEnumField("proto", 1, ISAKMP_protos),
520 FieldLenField("SPIsize", None, length_of="SPIs", fmt="B",
521 adjust=lambda pkt, x: x and x // len(pkt.SPIs)),
522 FieldLenField("SPIcount", None, count_of="SPIs", fmt="H"),
523 FieldListField("SPIs", [],
524 StrLenField("", "", length_from=lambda pkt: pkt.SPIsize),
525 count_from=lambda pkt: pkt.SPIcount),
526 ]
527
528
529bind_bottom_up(UDP, ISAKMP, dport=500)
530bind_bottom_up(UDP, ISAKMP, sport=500)
531bind_top_down(UDP, ISAKMP, dport=500, sport=500)
532
533bind_bottom_up(NON_ESP, ISAKMP)
534
535# Add bindings
536bind_top_down(_ISAKMP_class, ISAKMP_payload, next_payload=0)
537bind_layers(_ISAKMP_class, ISAKMP_payload_SA, next_payload=1)
538bind_layers(_ISAKMP_class, ISAKMP_payload_Proposal, next_payload=2)
539bind_layers(_ISAKMP_class, ISAKMP_payload_Transform, next_payload=3)
540bind_layers(_ISAKMP_class, ISAKMP_payload_KE, next_payload=4)
541bind_layers(_ISAKMP_class, ISAKMP_payload_ID, next_payload=5)
542# bind_layers(_ISAKMP_class, ISAKMP_payload_CERT, next_payload=6)
543# bind_layers(_ISAKMP_class, ISAKMP_payload_CR, next_payload=7)
544bind_layers(_ISAKMP_class, ISAKMP_payload_Hash, next_payload=8)
545bind_layers(_ISAKMP_class, ISAKMP_payload_SIG, next_payload=9)
546bind_layers(_ISAKMP_class, ISAKMP_payload_Nonce, next_payload=10)
547bind_layers(_ISAKMP_class, ISAKMP_payload_Notify, next_payload=11)
548bind_layers(_ISAKMP_class, ISAKMP_payload_Delete, next_payload=12)
549bind_layers(_ISAKMP_class, ISAKMP_payload_VendorID, next_payload=13)
550
551
552def ikescan(ip):
553 """Sends/receives a ISAMPK payload SA with payload proposal"""
554 pkt = IP(dst=ip)
555 pkt /= UDP()
556 pkt /= ISAKMP(init_cookie=RandString(8), exch_type=2)
557 pkt /= ISAKMP_payload_SA(prop=ISAKMP_payload_Proposal())
558 return sr(pkt)