Skip to content

Commit 7b053aa

Browse files
committed
[psp] Add support for the PSP protocol
PSP stands for PSP Security Protocol, and is a lightweight IPSec-Like implementation that was released by Google and is getting traction within data centers. This commit adds support for versions 0 & 1 of the protocol which use AES-GCM in 128 and 265 bits. Support was tested against the testing tool of the RFC which generated the same PCAPs that are now used for unit testing. Signed-off-by: Eyal Itkin <[email protected]>
1 parent 0648c0d commit 7b053aa

6 files changed

+275
-0
lines changed

scapy/contrib/psp.py

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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) 2025
5+
6+
# scapy.contrib.description = PSP Security Protocol
7+
# scapy.contrib.status = loads
8+
9+
r"""
10+
PSP layer
11+
=========
12+
13+
Example of use:
14+
15+
>>> payload = IP() / UDP() / Raw("A" * 9)
16+
>>> iv = b'\x01\x02\x03\x04\x05\x06\x07\x08'
17+
>>> spi = 0x11223344
18+
>>> key = b'\xFF\xEE\xDD\xCC\xBB\xAA\x99\x88\x77\x66\x55\x44\x33\x22\x11\x00'
19+
>>> psp_packet = PSP(nexthdr=4, cryptoffset=5, spi=spi, iv=iv, data=payload)
20+
>>> hexdump(psp_packet)
21+
0000 04 01 05 01 11 22 33 44 01 02 03 04 05 06 07 08 ....."3D........
22+
0010 45 00 00 25 00 01 00 00 40 11 7C C5 7F 00 00 01 E..%....@.|.....
23+
0020 7F 00 00 01 00 35 00 35 00 11 BB 5A 41 41 41 41 .....5.5...ZAAAA
24+
0030 41 41 41 41 41 AAAAA
25+
>>>
26+
>>> psp_packet.encrypt(key)
27+
>>> hexdump(psp_packet)
28+
0000 04 01 05 01 11 22 33 44 01 02 03 04 05 06 07 08 ....."3D........
29+
0010 45 00 00 25 00 01 00 00 40 11 7C C5 7F 00 00 01 E..%....@.|.....
30+
0020 7F 00 00 01 8A D9 3D 08 45 C7 70 67 5C DA C3 9B ......=.E.pg\...
31+
0030 86 17 62 A0 CF BD 8C 46 06 15 31 91 8A C5 C2 A8 ..b....F..1.....
32+
0040 9E A3 1B A8 F0 .....
33+
>>>
34+
>>> psp_packet.decrypt(key)
35+
>>> hexdump(psp_packet)
36+
0000 04 01 05 01 11 22 33 44 01 02 03 04 05 06 07 08 ....."3D........
37+
0010 45 00 00 25 00 01 00 00 40 11 7C C5 7F 00 00 01 E..%....@.|.....
38+
0020 7F 00 00 01 00 35 00 35 00 11 BB 5A 41 41 41 41 .....5.5...ZAAAA
39+
0030 41 41 41 41 41 AAAAA
40+
>>>
41+
42+
"""
43+
44+
from scapy.config import conf
45+
from scapy.error import log_loading
46+
from scapy.fields import (
47+
BitField,
48+
ByteField,
49+
ConditionalField,
50+
XIntField,
51+
XStrField,
52+
StrFixedLenField,
53+
)
54+
from scapy.packet import (
55+
Packet,
56+
bind_bottom_up,
57+
bind_top_down,
58+
)
59+
from scapy.layers.inet import UDP
60+
61+
###############################################################################
62+
if conf.crypto_valid:
63+
from cryptography.exceptions import InvalidTag
64+
from cryptography.hazmat.primitives.ciphers import (
65+
aead,
66+
)
67+
else:
68+
log_loading.info("Can't import python-cryptography v1.7+. "
69+
"Disabled PSP encryption/authentication.")
70+
71+
###############################################################################
72+
import struct
73+
74+
75+
class PSP(Packet):
76+
"""
77+
PSP Security Protocol
78+
79+
See https://github.com/google/psp/blob/main/doc/PSP_Arch_Spec.pdf
80+
"""
81+
name = 'PSP'
82+
83+
fields_desc = [
84+
ByteField('nexthdr', 0),
85+
ByteField('hdrextlen', 1),
86+
BitField("reserved", 0, 2),
87+
BitField("cryptoffset", 0, 6),
88+
BitField("sample", 0, 1),
89+
BitField("drop", 0, 1),
90+
BitField("version", 0, 4),
91+
BitField("is_virt", 0, 1),
92+
BitField("one_bit", 1, 1),
93+
XIntField('spi', 0x00),
94+
StrFixedLenField('iv', '\x00' * 8, 8),
95+
ConditionalField(XIntField("virtkey", 0x00), lambda pkt: pkt.is_virt == 1),
96+
ConditionalField(XIntField("sectoken", 0x00), lambda pkt: pkt.is_virt == 1),
97+
XStrField('data', None),
98+
]
99+
100+
def sanitize_cipher(self):
101+
"""
102+
Ensure we support the cipher to encrypt/decrypt this packet
103+
104+
:returns: the supported cipher suite
105+
:raise scapy.layers.psp.PSPCipherError: if the requested cipher
106+
is unsupported
107+
"""
108+
if self.version not in (0, 1):
109+
raise PSPCipherError('Can not encrypt/decrypt using unsupported version %s'
110+
% (self.version))
111+
return aead.AESGCM
112+
113+
def encrypt(self, key):
114+
"""
115+
Encrypt a PSP packet
116+
117+
:param key: the secret key used for encryption
118+
:raise scapy.layers.psp.PSPCipherError: if the requested cipher
119+
is unsupported
120+
"""
121+
cipher = self.sanitize_cipher()
122+
encrypt_start_offset = 16 + self.cryptoffset * 4
123+
iv = struct.pack("!L", self.spi) + self.iv
124+
plain = b''
125+
to_encrypt = bytes(self.data)
126+
self.data = b''
127+
psp_header = bytes(self)
128+
header_length = len(psp_header)
129+
# Header should always be fully plaintext
130+
if header_length < encrypt_start_offset:
131+
plain = to_encrypt[:encrypt_start_offset - header_length]
132+
to_encrypt = to_encrypt[encrypt_start_offset - header_length:]
133+
cipher = cipher(key)
134+
payload = cipher.encrypt(iv, to_encrypt, psp_header + plain)
135+
self.data = plain + payload
136+
137+
def decrypt(self, key):
138+
"""
139+
Decrypt a PSP packet
140+
141+
:param key: the secret key used for encryption
142+
:raise scapy.layers.psp.PSPIntegrityError: if the integrity check
143+
fails with an AEAD algorithm
144+
:raise scapy.layers.psp.PSPCipherError: if the requested cipher
145+
is unsupported
146+
"""
147+
cipher = self.sanitize_cipher()
148+
self.icv_size = 16
149+
iv = struct.pack("!L", self.spi) + self.iv
150+
data = self.data[:len(self.data) - self.icv_size]
151+
icv = self.data[len(self.data) - self.icv_size:]
152+
153+
decrypt_start_offset = 16 + self.cryptoffset * 4
154+
plain = b''
155+
to_decrypt = bytes(data)
156+
self.data = b''
157+
psp_header = bytes(self)
158+
header_length = len(psp_header)
159+
# Header should always be fully plaintext
160+
if header_length < decrypt_start_offset:
161+
plain = to_decrypt[:decrypt_start_offset - header_length]
162+
to_decrypt = to_decrypt[decrypt_start_offset - header_length:]
163+
cipher = cipher(key)
164+
try:
165+
data = cipher.decrypt(iv, to_decrypt + icv, psp_header + plain)
166+
self.data = plain + data
167+
except InvalidTag as err:
168+
raise PSPIntegrityError(err)
169+
170+
171+
bind_bottom_up(UDP, PSP, dport=1000)
172+
bind_bottom_up(UDP, PSP, sport=1000)
173+
bind_top_down(UDP, PSP, dport=1000, sport=1000)
174+
175+
###############################################################################
176+
177+
178+
class PSPCipherError(Exception):
179+
"""
180+
Error risen when the cipher is unsupported.
181+
"""
182+
pass
183+
184+
185+
class PSPIntegrityError(Exception):
186+
"""
187+
Error risen when the integrity check fails.
188+
"""
189+
pass

test/contrib/psp.uts

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# PSP unit tests
2+
# run with:
3+
# test/run_tests -P "load_contrib('psp')" -t test/contrib/psp.uts -F
4+
5+
% Regression tests for the PSP layer
6+
7+
###############
8+
##### PSP #####
9+
###############
10+
11+
+ PSP tests
12+
13+
= PSP layer
14+
15+
example_plain_packet = import_hexcap('''\
16+
0000 04 01 05 01 11 22 33 44 01 02 03 04 05 06 07 08 ....."3D........
17+
0010 45 00 00 25 00 01 00 00 40 11 7C C5 7F 00 00 01 E..%....@.|.....
18+
0020 7F 00 00 01 00 35 00 35 00 11 BB 5A 41 41 41 41 .....5.5...ZAAAA
19+
0030 41 41 41 41 41 AAAAA
20+
''')
21+
psp_packet = PSP(example_plain_packet)
22+
assert psp_packet.nexthdr == 4
23+
assert psp_packet.hdrextlen == 1
24+
assert psp_packet.cryptoffset == 5
25+
assert psp_packet.version == 0
26+
assert psp_packet.spi == 0x11223344
27+
assert psp_packet.iv == b'\x01\x02\x03\x04\x05\x06\x07\x08'
28+
29+
payload = IP(psp_packet.data)
30+
assert payload[UDP].sport == 53
31+
assert payload[UDP].dport == 53
32+
assert bytes(payload[Raw]) == b"A" * 9
33+
34+
= PSP Usage Example
35+
36+
payload = IP() / UDP() / Raw("A" * 9)
37+
iv = b'\x01\x02\x03\x04\x05\x06\x07\x08'
38+
spi = 0x11223344
39+
key = b'\xFF\xEE\xDD\xCC\xBB\xAA\x99\x88\x77\x66\x55\x44\x33\x22\x11\x00'
40+
psp_packet = PSP(nexthdr=4, cryptoffset=5, spi=spi, iv=iv, data=payload)
41+
hexdump(psp_packet)
42+
expected_orig_packet = import_hexcap(r'''\
43+
0000 04 01 05 01 11 22 33 44 01 02 03 04 05 06 07 08 ....."3D........
44+
0010 45 00 00 25 00 01 00 00 40 11 7C C5 7F 00 00 01 E..%....@.|.....
45+
0020 7F 00 00 01 00 35 00 35 00 11 BB 5A 41 41 41 41 .....5.5...ZAAAA
46+
0030 41 41 41 41 41 AAAAA
47+
''')
48+
assert bytes(psp_packet) == bytes(expected_orig_packet)
49+
# Now let's encrypt it
50+
psp_packet.encrypt(key)
51+
hexdump(psp_packet)
52+
assert bytes(psp_packet) == import_hexcap(r'''\
53+
0000 04 01 05 01 11 22 33 44 01 02 03 04 05 06 07 08 ....."3D........
54+
0010 45 00 00 25 00 01 00 00 40 11 7C C5 7F 00 00 01 E..%....@.|.....
55+
0020 7F 00 00 01 8A D9 3D 08 45 C7 70 67 5C DA C3 9B ......=.E.pg\...
56+
0030 86 17 62 A0 CF BD 8C 46 06 15 31 91 8A C5 C2 A8 ..b....F..1.....
57+
0040 9E A3 1B A8 F0 .....
58+
''')
59+
# Now let's decrypt it back
60+
psp_packet.decrypt(key)
61+
hexdump(psp_packet)
62+
assert bytes(psp_packet) == bytes(expected_orig_packet)
63+
64+
= PSP RFC Test - Version 0, no VC
65+
key_128 = b'\x39\x46\xDA\x25\x54\xEA\xE4\x6A\xD1\xEF\x77\xA6\x43\x72\xED\xC4'
66+
spi = 0x9A345678
67+
IV = b'\x00\x00\x00\x00\x00\x00\x00\x01'
68+
plaintext_packet = rdpcap(scapy_path("/test/pcaps/psp_v4_cleartext.pcap"))[0]
69+
encrypted_packet = rdpcap(scapy_path("/test/pcaps/psp_v4_encrypt_transport_crypt_off_128.pcap"))[0]
70+
psp_packet = PSP(nexthdr=0x11, cryptoffset=1, spi=spi, iv=IV, data=plaintext_packet[UDP])
71+
psp_packet.encrypt(key_128)
72+
assert bytes(psp_packet) == bytes(encrypted_packet[PSP])
73+
74+
= PSP RFC Test - Version 1, no VC
75+
key_256 = b'\xFA\x00\xF6\x09\xDF\x60\x20\x28\x9A\x1C\x93\xD6\x02\x70\x81\xA6\x37\xAD\x45\xB2\x4A\x55\x76\xB3\x6E\x6F\x49\xDD\x43\x11\x4D\x80'
76+
# SPI and IV are the same as before
77+
encrypted_packet = rdpcap(scapy_path("/test/pcaps/psp_v4_encrypt_transport_crypt_off_256.pcap"))[0]
78+
psp_packet = PSP(nexthdr=0x11, cryptoffset=1, version=1, spi=spi, iv=IV, data=plaintext_packet[UDP])
79+
psp_packet.encrypt(key_256)
80+
assert bytes(psp_packet) == bytes(encrypted_packet[PSP])
81+
82+
= PSP RFC Test - Version 0, with VC
83+
encrypted_packet = rdpcap(scapy_path("/test/pcaps/psp_v4_encrypt_transport_crypt_off_128_vc.pcap"))[0]
84+
psp_packet = PSP(nexthdr=0x11, hdrextlen=2, cryptoffset=3, is_virt=1, spi=spi, iv=IV, data=plaintext_packet[UDP])
85+
psp_packet.encrypt(key_128)
86+
assert bytes(psp_packet) == bytes(encrypted_packet[PSP])

test/pcaps/psp_v4_cleartext.pcap

1.44 KB
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)