Skip to content

Commit 8ea0169

Browse files
committed
[psp] Add support for the PSP
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. Signed-off-by: Eyal Itkin <[email protected]>
1 parent 7275516 commit 8ea0169

File tree

2 files changed

+187
-0
lines changed

2 files changed

+187
-0
lines changed

scapy/config.py

+1
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,7 @@ class Conf(ConfClass):
10571057
'ppi',
10581058
'ppp',
10591059
'pptp',
1060+
'psp',
10601061
'radius',
10611062
'rip',
10621063
'rtp',

scapy/layers/psp.py

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

0 commit comments

Comments
 (0)