Skip to content

Commit 117394b

Browse files
committed
T8136: IPSEC PPK Support
1 parent 014ef8b commit 117394b

File tree

9 files changed

+278
-3
lines changed

9 files changed

+278
-3
lines changed

data/templates/ipsec/swanctl.conf.j2

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,36 @@ secrets {
9191
{% endif %}
9292
{% if psk_config.secret_type is vyos_defined('base64') %}
9393
secret = 0s{{ psk_config.secret }}
94+
{% elif psk_config.secret_type is vyos_defined('hex') %}
95+
secret = 0x{{ psk_config.secret }}
9496
{% elif psk_config.secret_type is vyos_defined('plaintext') %}
9597
secret = "{{ psk_config.secret }}"
9698
{% endif %}
9799
}
98100
{% endfor %}
99101
{% endif %}
100102

103+
{% if authentication.ppk is vyos_defined %}
104+
{% for ppk, ppk_config in authentication.ppk.items() %}
105+
ppk-{{ ppk }} {
106+
{% if ppk_config.id is vyos_defined %}
107+
# ID's from auth ppk <tag> id xxx
108+
{% for id in ppk_config.id %}
109+
{% set gen_uuid = '' | generate_uuid4 %}
110+
id-{{ gen_uuid }} = "{{ id }}"
111+
{% endfor %}
112+
{% endif %}
113+
{% if ppk_config.secret_type is vyos_defined('base64') %}
114+
secret = 0s{{ ppk_config.secret }}
115+
{% elif ppk_config.secret_type is vyos_defined('hex') %}
116+
secret = 0x{{ ppk_config.secret }}
117+
{% elif ppk_config.secret_type is vyos_defined('plaintext') %}
118+
secret = "{{ ppk_config.secret }}"
119+
{% endif %}
120+
}
121+
{% endfor %}
122+
{% endif %}
123+
101124
{% if remote_access.connection is vyos_defined %}
102125
{% for ra, ra_conf in remote_access.connection.items() if ra_conf.disable is not vyos_defined %}
103126
{% if ra_conf.authentication.server_mode is vyos_defined('pre-shared-secret') %}

data/templates/ipsec/swanctl/peer.j2

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
{# peer needs to reference the global IKE configuration for certain values #}
44
{% set ike = ike_group[peer_conf.ike_group] %}
55
{{ name }} {
6+
{% if peer_conf.ppk.id is vyos_defined %}
7+
ppk_id = {{ peer_conf.ppk.id }}
8+
{% endif %}
9+
{% if peer_conf.ppk.required is vyos_defined %}
10+
ppk_required = yes
11+
{% endif %}
12+
{% if peer_conf.childless is vyos_defined %}
13+
childless = {{ peer_conf.childless }}
14+
{% endif %}
615
proposals = {{ ike | get_esp_ike_cipher | join(',') }}
716
version = {{ ike.key_exchange[4:] if ike.key_exchange is vyos_defined else "0" }}
817
{% if peer_conf.virtual_address is vyos_defined %}

data/templates/ipsec/swanctl/remote_access.j2

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
{% set ike = ike_group[rw_conf.ike_group] %}
44
{% set esp = esp_group[rw_conf.esp_group] %}
55
ra-{{ name }} {
6+
{% if rw_conf.ppk.id is vyos_defined %}
7+
ppk_id = {{ rw_conf.ppk.id }}
8+
{% endif %}
9+
{% if rw_conf.ppk.required is vyos_defined %}
10+
ppk_required = yes
11+
{% endif %}
12+
{% if rw_conf.childless is vyos_defined %}
13+
childless = {{ rw_conf.childless }}
14+
{% endif %}
615
remote_addrs = %any
716
local_addrs = {{ rw_conf.local_address if rw_conf.local_address is not vyos_defined('any') else '%any' }} # dhcp:{{ rw_conf.dhcp_interface if rw_conf.dhcp_interface is vyos_defined else 'no' }}
817
proposals = {{ ike_group[rw_conf.ike_group] | get_esp_ike_cipher | join(',') }}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!-- include start from ipsec/childless.xml.i -->
2+
<leafNode name="childless">
3+
<properties>
4+
<help>Enable support for childless IKE_SA initiation</help>
5+
<completionHelp>
6+
<list>allow prefer force never</list>
7+
</completionHelp>
8+
<valueHelp>
9+
<format>allow</format>
10+
<description>Responder will allow childless IKE_SA, but initiator will not create childless connection</description>
11+
</valueHelp>
12+
<valueHelp>
13+
<format>prefer</format>
14+
<description>Responder will allow childless IKE_SA, and initiator will make childless connection if supported by responder</description>
15+
</valueHelp>
16+
<valueHelp>
17+
<format>force</format>
18+
<description>Require the use of childless IKE_SA</description>
19+
</valueHelp>
20+
<valueHelp>
21+
<format>never</format>
22+
<description>Disable support for childless IKE_SAs as responder</description>
23+
</valueHelp>
24+
<constraint>
25+
<regex>(allow|prefer|force|never)</regex>
26+
</constraint>
27+
</properties>
28+
</leafNode>
29+
<!-- include end -->
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!-- include start from ipsec/ppk.xml.i -->
2+
<node name="ppk">
3+
<properties>
4+
<help>Post-quantum preshared key</help>
5+
</properties>
6+
<children>
7+
<leafNode name="id">
8+
<properties>
9+
<help>Post-quantum preshared key for this connection</help>
10+
<valueHelp>
11+
<format>txt</format>
12+
<description>ID used for PPK</description>
13+
</valueHelp>
14+
</properties>
15+
</leafNode>
16+
<leafNode name="required">
17+
<properties>
18+
<help>Require a valid PPK for connection to establish</help>
19+
<valueless/>
20+
</properties>
21+
</leafNode>
22+
</children>
23+
</node>
24+
<!-- include end -->

interface-definitions/vpn_ipsec.xml.in

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,48 @@
4545
<properties>
4646
<help>Secret type</help>
4747
<completionHelp>
48-
<list>base64 plaintext</list>
48+
<list>base64 hex plaintext</list>
4949
</completionHelp>
5050
<constraint>
51-
<regex>(base64|plaintext)</regex>
51+
<regex>(base64|hex|plaintext)</regex>
52+
</constraint>
53+
</properties>
54+
<defaultValue>plaintext</defaultValue>
55+
</leafNode>
56+
</children>
57+
</tagNode>
58+
<tagNode name="ppk">
59+
<properties>
60+
<help>Post-quantum preshared key name</help>
61+
</properties>
62+
<children>
63+
<leafNode name="id">
64+
<properties>
65+
<help>ID for PPK</help>
66+
<valueHelp>
67+
<format>txt</format>
68+
<description>ID used for PPK</description>
69+
</valueHelp>
70+
<multi/>
71+
</properties>
72+
</leafNode>
73+
<leafNode name="secret">
74+
<properties>
75+
<help>Post-quantum preshared secret key</help>
76+
<valueHelp>
77+
<format>txt</format>
78+
<description>Post-quantum preshared secret key</description>
79+
</valueHelp>
80+
</properties>
81+
</leafNode>
82+
<leafNode name="secret-type">
83+
<properties>
84+
<help>Secret type</help>
85+
<completionHelp>
86+
<list>base64 hex plaintext</list>
87+
</completionHelp>
88+
<constraint>
89+
<regex>(base64|hex|plaintext)</regex>
5290
</constraint>
5391
</properties>
5492
<defaultValue>plaintext</defaultValue>
@@ -899,13 +937,15 @@
899937
#include <include/ipsec/authentication-pre-shared-secret.xml.i>
900938
</children>
901939
</node>
940+
#include <include/ipsec/childless.xml.i>
902941
#include <include/generic-description.xml.i>
903942
#include <include/generic-disable-node.xml.i>
904943
#include <include/ipsec/esp-group.xml.i>
905944
#include <include/ipsec/ike-group.xml.i>
906945
#include <include/ipsec/local-address.xml.i>
907946
#include <include/dhcp-interface.xml.i>
908947
#include <include/ipsec/local-traffic-selector.xml.i>
948+
#include <include/ipsec/ppk.xml.i>
909949
#include <include/ipsec/replay-window.xml.i>
910950
#include <include/ipsec/bind.xml.i>
911951
<leafNode name="timeout">
@@ -1156,6 +1196,7 @@
11561196
</leafNode>
11571197
</children>
11581198
</node>
1199+
#include <include/ipsec/childless.xml.i>
11591200
<leafNode name="connection-type">
11601201
<properties>
11611202
<help>Connection type</help>
@@ -1220,6 +1261,7 @@
12201261
</properties>
12211262
</leafNode>
12221263
#include <include/ipsec/local-address.xml.i>
1264+
#include <include/ipsec/ppk.xml.i>
12231265
#include <include/ipsec/remote-address.xml.i>
12241266
#include <include/ipsec/replay-window.xml.i>
12251267
<tagNode name="tunnel">

smoketest/scripts/cli/test_vpn_ipsec.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
esp_group = 'MyESPGroup'
4848
ike_group = 'MyIKEGroup'
4949
secret = 'MYSECRETKEY'
50+
ppk_secret_hex = '55c2ebca1bada7ac0e4e1390a8dbb563cefea0c7bd59f4f2c86a627f5927fb90'
5051
PROCESS_NAME = 'charon-systemd'
5152
regex_uuid4 = '[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}'
5253

@@ -668,6 +669,104 @@ def test_site_to_site_vti_ts_afi(self):
668669
for line in swanctl_conf_lines:
669670
self.assertIn(line, swanctl_conf)
670671

672+
def test_site_to_site_nist_800_77_cnsa_1_with_ppk(self):
673+
# Setup IKE group
674+
self.cli_set(base_path + ['ike-group', 'cnsa1-ike', 'key-exchange', 'ikev2'])
675+
self.cli_set(base_path + ['ike-group', 'cnsa1-ike', 'lifetime', '86400'])
676+
self.cli_set(base_path + ['ike-group', 'cnsa1-ike', 'proposal', '10', 'dh-group', '20'])
677+
self.cli_set(base_path + ['ike-group', 'cnsa1-ike', 'proposal', '10', 'encryption', 'aes256gcm128'])
678+
self.cli_set(base_path + ['ike-group', 'cnsa1-ike', 'proposal', '10', 'hash', 'sha384'])
679+
self.cli_set(base_path + ['ike-group', 'cnsa1-ike', 'proposal', '10', 'prf', 'prfsha384'])
680+
681+
# Setup ESP group
682+
self.cli_set(base_path + ['esp-group', 'cnsa1-esp', 'lifetime', '28800'])
683+
self.cli_set(base_path + ['esp-group', 'cnsa1-esp', 'mode', 'tunnel'])
684+
self.cli_set(base_path + ['esp-group', 'cnsa1-esp', 'pfs', 'dh-group20'])
685+
self.cli_set(base_path + ['esp-group', 'cnsa1-esp', 'proposal', '10', 'encryption', 'aes256gcm128'])
686+
self.cli_set(base_path + ['esp-group', 'cnsa1-esp', 'proposal', '10', 'hash', 'sha384'])
687+
688+
local_address = '192.0.2.10'
689+
690+
# vpn ipsec auth psk <tag> id <x.x.x.x>
691+
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id])
692+
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id])
693+
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_address])
694+
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip])
695+
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret])
696+
697+
# vpn ipsec auth ppk <tag> id <name>
698+
self.cli_set(base_path + ['authentication', 'ppk', connection_name, 'id', 'ppk-test'])
699+
self.cli_set(base_path + ['authentication', 'ppk', connection_name, 'secret', ppk_secret_hex])
700+
self.cli_set(base_path + ['authentication', 'ppk', connection_name, 'secret-type', 'hex'])
701+
702+
# Site to site
703+
peer_base_path = base_path + ['site-to-site', 'peer', connection_name]
704+
705+
self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
706+
707+
# Set childless IKE_INIT to prefer
708+
self.cli_set(peer_base_path + ['childless', 'prefer'])
709+
710+
self.cli_set(peer_base_path + ['default-esp-group', 'cnsa1-esp'])
711+
self.cli_set(peer_base_path + ['ike-group', 'cnsa1-ike'])
712+
self.cli_set(peer_base_path + ['local-address', local_address])
713+
714+
# Require use of valid PPK
715+
self.cli_set(peer_base_path + ['ppk', 'id', 'ppk-test'])
716+
self.cli_set(peer_base_path + ['ppk', 'required'])
717+
718+
self.cli_set(peer_base_path + ['remote-address', peer_ip])
719+
self.cli_set(peer_base_path + ['tunnel', '1', 'local', 'prefix', '172.16.10.0/24'])
720+
self.cli_set(peer_base_path + ['tunnel', '1', 'remote', 'prefix', '172.17.10.0/24'])
721+
722+
self.cli_commit()
723+
724+
# Verify strongSwan configuration
725+
swanctl_conf = read_file(swanctl_file)
726+
swanctl_conf_lines = [
727+
f'ppk_id = ppk-test',
728+
f'ppk_required = yes',
729+
f'childless = prefer',
730+
f'version = 2',
731+
f'auth = psk',
732+
f'rekey_time = 86400s',
733+
f'proposals = aes256gcm128-sha384-prfsha384-ecp384',
734+
f'esp_proposals = aes256gcm128-sha384-ecp384',
735+
f'life_time = 28800s', # default value
736+
f'local_addrs = {local_address} # dhcp:no',
737+
f'remote_addrs = {peer_ip}',
738+
f'mode = tunnel',
739+
f'{connection_name}-tunnel-1',
740+
f'local_ts = 172.16.10.0/24',
741+
f'remote_ts = 172.17.10.0/24',
742+
f'mode = tunnel',
743+
f'replay_window = 32',
744+
]
745+
for line in swanctl_conf_lines:
746+
self.assertIn(line, swanctl_conf)
747+
748+
# if dpd is not specified it should not be enabled (see T6599)
749+
swanctl_unexpected_lines = [
750+
'dpd_timeout',
751+
'dpd_delay',
752+
]
753+
754+
for unexpected_line in swanctl_unexpected_lines:
755+
self.assertNotIn(unexpected_line, swanctl_conf)
756+
757+
swanctl_secrets_lines = [
758+
f'id-{regex_uuid4} = "{local_id}"',
759+
f'id-{regex_uuid4} = "{remote_id}"',
760+
f'id-{regex_uuid4} = "{local_address}"',
761+
f'id-{regex_uuid4} = "{peer_ip}"',
762+
f'secret = "{secret}"',
763+
f'ppk-{connection_name}',
764+
f'id-{regex_uuid4} = "ppk-test"',
765+
f'secret = 0x{ppk_secret_hex}'
766+
]
767+
for line in swanctl_secrets_lines:
768+
self.assertRegex(swanctl_conf, fr'{line}')
769+
671770

672771
def test_dmvpn(self):
673772
ike_lifetime = '3600'

src/conf_mode/vpn_ipsec.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,11 +261,23 @@ def verify(ipsec):
261261
if not ipsec or 'deleted' in ipsec:
262262
return
263263

264+
#T8136 PPK support; keep a list of PPK IDs
265+
ppk_ids = []
266+
264267
if 'authentication' in ipsec:
265268
if 'psk' in ipsec['authentication']:
266269
for psk, psk_config in ipsec['authentication']['psk'].items():
267270
if 'id' not in psk_config or 'secret' not in psk_config:
268271
raise ConfigError(f'Authentication psk "{psk}" missing "id" or "secret"')
272+
#T8136 PPK Support; Check that PPK has an ID and secret defined, and ID is unique
273+
if 'ppk' in ipsec['authentication']:
274+
for ppk, ppk_config in ipsec['authentication']['ppk'].items():
275+
if 'id' not in ppk_config or 'secret' not in ppk_config:
276+
raise ConfigError(f'Authentication PPK "{ppk}" missing "id" or "secret"')
277+
for ppkID in ppk_config['id']:
278+
if ppkID in ppk_ids:
279+
raise ConfigError(f'Authentication PPK "{ppk}" has duplicate ID "{ppkID}" from another PPK. IDs should be unique.')
280+
ppk_ids.append(ppkID)
269281

270282
if 'interface' in ipsec:
271283
tmp = re.compile(dynamic_interface_pattern)
@@ -445,6 +457,17 @@ def verify(ipsec):
445457
elif 'pool' not in ipsec['remote_access'] or pool not in ipsec['remote_access']['pool']:
446458
raise ConfigError(f'Requested pool "{pool}" does not exist!')
447459

460+
#T8136 IPSEC PPK Support
461+
#PPKs and Childless only works with IKEv2. Check that ike-group is v2 if either option is enabled. Check that PPK ID was actually defined in authentication.
462+
if 'ppk' in ra_conf:
463+
ike = ra_conf['ike_group']
464+
if dict_search(f'ike_group.{ike}.key_exchange', ipsec) != 'ikev2':
465+
raise ConfigError(f'Post-quantum preshared keys must be used with IKEv2! IKEv2 key-exchange not set in ike-group "{ike}".')
466+
if 'childless' in ra_conf:
467+
ike = ra_conf['ike_group']
468+
if dict_search(f'ike_group.{ike}.key_exchange', ipsec) != 'ikev2':
469+
raise ConfigError(f'Childless IKE SAs be used with IKEv2! IKEv2 key-exchange not set in ike-group "{ike}".')
470+
448471
if 'pool' in ipsec['remote_access']:
449472
pool_networks = []
450473
for pool, pool_config in ipsec['remote_access']['pool'].items():
@@ -653,6 +676,17 @@ def verify(ipsec):
653676
f'for ESP proposal {proposal} on tunnel {tunnel} for site-to-site peer {peer} with VPP'
654677
)
655678

679+
#T8136 IPSEC PPK Support
680+
#PPKs and Childless only works with IKEv2. Check that ike-group is v2 if either option is enabled. Check that PPK ID was actually defined in authentication.
681+
if 'ppk' in peer_conf:
682+
ike = peer_conf['ike_group']
683+
if dict_search(f'ike_group.{ike}.key_exchange', ipsec) != 'ikev2':
684+
raise ConfigError(f'Post-quantum preshared keys must be used with IKEv2! IKEv2 key-exchange not set in ike-group "{ike}".')
685+
if 'childless' in peer_conf:
686+
ike = peer_conf['ike_group']
687+
if dict_search(f'ike_group.{ike}.key_exchange', ipsec) != 'ikev2':
688+
raise ConfigError(f'Childless IKE SAs be used with IKEv2! IKEv2 key-exchange not set in ike-group "{ike}".')
689+
656690

657691
def cleanup_pki_files():
658692
for path in [CERT_PATH, CA_PATH, CRL_PATH, KEY_PATH, PUBKEY_PATH]:

src/op_mode/vpn_ike_sa.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,13 @@ def ike_sa(peer, nat):
4949
local_str = f'{s(sa["local-host"])} {s(sa["local-id"])}' if s(sa['local-id']) != '%any' else s(sa["local-host"])
5050
print(ike_sa_peer_prefix)
5151
print('%-39s %-39s' % (remote_str, local_str))
52-
state = 'up' if 'state' in sa and s(sa['state']) == 'ESTABLISHED' else 'down'
52+
if 'state' in sa and s(sa['state']) == 'ESTABLISHED':
53+
if 'ppk' in sa and s(sa['ppk']) == 'yes':
54+
state = 'up-ppk'
55+
else:
56+
state = 'up'
57+
else:
58+
state = 'down'
5359
version = 'IKEv' + s(sa['version'])
5460
encryption = f'{s(sa["encr-alg"])}' if 'encr-alg' in sa else 'n/a'
5561
if 'encr-keysize' in sa:

0 commit comments

Comments
 (0)