From e97d86e619e134f4dfda06efb7df4a3296d17b95 Mon Sep 17 00:00:00 2001
From: Lucas Christian <lucas@lucasec.com>
Date: Mon, 29 Jul 2024 23:22:05 -0700
Subject: T6617: T6618: vpn ipsec remote-access: fix profile generators

---
 data/templates/ipsec/ios_profile.j2     |  9 +++-
 data/templates/ipsec/windows_profile.j2 |  2 +-
 src/op_mode/ikev2_profile_generator.py  | 85 ++++++++++++++++++++++++++++-----
 3 files changed, 83 insertions(+), 13 deletions(-)

diff --git a/data/templates/ipsec/ios_profile.j2 b/data/templates/ipsec/ios_profile.j2
index 935acbf8e..966fad433 100644
--- a/data/templates/ipsec/ios_profile.j2
+++ b/data/templates/ipsec/ios_profile.j2
@@ -55,9 +55,11 @@
                 <!-- The server is authenticated using a certificate -->
                 <key>AuthenticationMethod</key>
                 <string>Certificate</string>
+{% if authentication.client_mode.startswith("eap") %}
                 <!-- The client uses EAP to authenticate -->
                 <key>ExtendedAuthEnabled</key>
                 <integer>1</integer>
+{% endif %}
                 <!-- The next two dictionaries are optional (as are the keys in them), but it is recommended to specify them as the default is to use 3DES.
                      IMPORTANT: Because only one proposal is sent (even if nothing is configured here) it must match the server configuration -->
                 <key>IKESecurityAssociationParameters</key>
@@ -78,9 +80,14 @@
                     <string>{{ esp_encryption.encryption }}</string>
                     <key>IntegrityAlgorithm</key>
                     <string>{{ esp_encryption.hash }}</string>
+{% if esp_encryption.pfs is vyos_defined %}
                     <key>DiffieHellmanGroup</key>
-                    <integer>{{ ike_encryption.dh_group }}</integer>
+                    <integer>{{ esp_encryption.pfs }}</integer>
+{% endif %}
                 </dict>
+                <!-- Controls whether the client offers Perfect Forward Secrecy (PFS). This should be set to match the server. -->
+                <key>EnablePFS</key>
+                <integer>{{ '1' if esp_encryption.pfs is vyos_defined else '0' }}</integer>
             </dict>
         </dict>
 {% if ca_certificates is vyos_defined %}
diff --git a/data/templates/ipsec/windows_profile.j2 b/data/templates/ipsec/windows_profile.j2
index 8c26944be..b5042f987 100644
--- a/data/templates/ipsec/windows_profile.j2
+++ b/data/templates/ipsec/windows_profile.j2
@@ -1,4 +1,4 @@
 Remove-VpnConnection -Name "{{ vpn_name }}" -Force -PassThru
 
 Add-VpnConnection -Name "{{ vpn_name }}" -ServerAddress "{{ remote }}" -TunnelType "Ikev2"
-Set-VpnConnectionIPsecConfiguration -ConnectionName "{{ vpn_name }}" -AuthenticationTransformConstants {{ ike_encryption.encryption }} -CipherTransformConstants {{ ike_encryption.encryption }} -EncryptionMethod {{ esp_encryption.encryption }} -IntegrityCheckMethod {{ esp_encryption.hash }} -PfsGroup None -DHGroup "Group{{ ike_encryption.dh_group }}" -PassThru -Force
+Set-VpnConnectionIPsecConfiguration -ConnectionName "{{ vpn_name }}" -AuthenticationTransformConstants {{ ike_encryption.encryption }} -CipherTransformConstants {{ ike_encryption.encryption }} -EncryptionMethod {{ esp_encryption.encryption }} -IntegrityCheckMethod {{ esp_encryption.hash }} -PfsGroup {{ esp_encryption.pfs }} -DHGroup {{ ike_encryption.dh_group }} -PassThru -Force
diff --git a/src/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py
index b193d8109..cf2bc6d5c 100755
--- a/src/op_mode/ikev2_profile_generator.py
+++ b/src/op_mode/ikev2_profile_generator.py
@@ -105,10 +105,39 @@ vyos2windows_integrity = {
 }
 
 # IOS 14.2 and later do no support dh-group 1,2 and 5. Supported DH groups would
-# be: 14, 15, 16, 17, 18, 19, 20, 21, 31
-ios_supported_dh_groups = ['14', '15', '16', '17', '18', '19', '20', '21', '31']
-# Windows 10 only allows a limited set of DH groups
-windows_supported_dh_groups = ['1', '2', '14', '24']
+# be: 14, 15, 16, 17, 18, 19, 20, 21, 31, 32
+vyos2apple_dh_group = {
+    '14' : '14',
+    '15' : '15',
+    '16' : '16',
+    '17' : '17',
+    '18' : '18',
+    '19' : '19',
+    '20' : '20',
+    '21' : '21',
+    '31' : '31',
+    '32' : '32'
+}
+
+# Newer versions of Windows support groups 19 and 20, albeit under a different naming convention
+vyos2windows_dh_group = {
+    '1'  : 'Group1',
+    '2'  : 'Group2',
+    '14' : 'Group14',
+    '19' : 'ECP256',
+    '20' : 'ECP384',
+    '24' : 'Group24'
+}
+
+# For PFS, Windows also has its own inconsistent naming scheme for each group
+vyos2windows_pfs_group = {
+    '1'  : 'PFS1',
+    '2'  : 'PFS2',
+    '14' : 'PFS2048',
+    '19' : 'ECP256',
+    '20' : 'ECP384',
+    '24' : 'PFS24'
+}
 
 parser = argparse.ArgumentParser()
 parser.add_argument('--os', const='all', nargs='?', choices=['ios', 'windows'], help='Operating system used for config generation', required=True)
@@ -181,7 +210,7 @@ if args.os == 'ios':
     # https://stackoverflow.com/a/9427216
     data['ca_certificates'] = [dict(t) for t in {tuple(d.items()) for d in data['ca_certificates']}]
 
-esp_proposals = conf.get_config_dict(ipsec_base + ['esp-group', data['esp_group'], 'proposal'],
+esp_group = conf.get_config_dict(ipsec_base + ['esp-group', data['esp_group']],
                                      key_mangling=('-', '_'), get_first_key=True)
 ike_proposal = conf.get_config_dict(ipsec_base + ['ike-group', data['ike_group'], 'proposal'],
                                     key_mangling=('-', '_'), get_first_key=True)
@@ -192,7 +221,29 @@ ike_proposal = conf.get_config_dict(ipsec_base + ['ike-group', data['ike_group']
 
 vyos2client_cipher = vyos2apple_cipher if args.os == 'ios' else vyos2windows_cipher;
 vyos2client_integrity = vyos2apple_integrity if args.os == 'ios' else vyos2windows_integrity;
-supported_dh_groups = ios_supported_dh_groups if args.os == 'ios' else windows_supported_dh_groups;
+vyos2client_dh_group = vyos2apple_dh_group if args.os == 'ios' else vyos2windows_dh_group
+
+def transform_pfs(pfs, ike_dh_group):
+    pfs_enabled = (pfs != 'disable')
+    if pfs == 'enable':
+        pfs_dh_group = ike_dh_group
+    elif pfs.startswith('dh-group'):
+        pfs_dh_group = pfs.removeprefix('dh-group')
+
+    if args.os == 'ios':
+        if pfs_enabled:
+            if pfs_dh_group not in set(vyos2apple_dh_group):
+                exit(f'The PFS group configured for "{args.connection}" is not supported by the client!')
+            return pfs_dh_group
+        else:
+            return None
+    else:
+        if pfs_enabled:
+            if pfs_dh_group not in set(vyos2windows_pfs_group):
+                exit(f'The PFS group configured for "{args.connection}" is not supported by the client!')
+            return vyos2windows_pfs_group[ pfs_dh_group ]
+        else:
+            return 'None'
 
 # Create a dictionary containing client conform IKE settings
 ike = {}
@@ -201,24 +252,28 @@ for _, proposal in ike_proposal.items():
     if {'dh_group', 'encryption', 'hash'} <= set(proposal):
         if (proposal['encryption'] in set(vyos2client_cipher) and
             proposal['hash'] in set(vyos2client_integrity) and
-            proposal['dh_group'] in set(supported_dh_groups)):
+            proposal['dh_group'] in set(vyos2client_dh_group)):
 
             # We 're-code' from the VyOS IPsec proposals to the Apple naming scheme
             proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ]
             proposal['hash'] = vyos2client_integrity[ proposal['hash'] ]
+            # DH group will need to be transformed later after we calculate PFS group
 
             ike.update( { str(count) : proposal } )
             count += 1
 
-# Create a dictionary containing Apple conform ESP settings
+# Create a dictionary containing client conform ESP settings
 esp = {}
 count = 1
-for _, proposal in esp_proposals.items():
+for _, proposal in esp_group['proposal'].items():
     if {'encryption', 'hash'} <= set(proposal):
         if proposal['encryption'] in set(vyos2client_cipher) and proposal['hash'] in set(vyos2client_integrity):
             # We 're-code' from the VyOS IPsec proposals to the Apple naming scheme
             proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ]
             proposal['hash'] = vyos2client_integrity[ proposal['hash'] ]
+            # Copy PFS setting from the group, if present (we will need to
+            # transform this later once the IKE group is selected)
+            proposal['pfs'] = esp_group.get('pfs', 'enable')
 
             esp.update( { str(count) : proposal } )
             count += 1
@@ -230,8 +285,10 @@ try:
             tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}, DH group {options["dh_group"]}\n'
         tmp += '\nSelect one of the above IKE groups: '
         data['ike_encryption'] = ike[ ask_input(tmp, valid_responses=list(ike)) ]
-    else:
+    elif len(ike) == 1:
         data['ike_encryption'] = ike['1']
+    else:
+        exit(f'None of the configured IKE proposals for "{args.connection}" are supported by the client!')
 
     if len(esp) > 1:
         tmp = '\n'
@@ -239,12 +296,18 @@ try:
             tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}\n'
         tmp += '\nSelect one of the above ESP groups: '
         data['esp_encryption'] = esp[ ask_input(tmp, valid_responses=list(esp)) ]
-    else:
+    elif len(esp) == 1:
         data['esp_encryption'] = esp['1']
+    else:
+        exit(f'None of the configured ESP  proposals for "{args.connection}" are supported by the client!')
 
 except KeyboardInterrupt:
     exit("Interrupted")
 
+# Transform the DH and PFS groups now that all selections are known
+data['esp_encryption']['pfs'] = transform_pfs(data['esp_encryption']['pfs'], data['ike_encryption']['dh_group'])
+data['ike_encryption']['dh_group'] = vyos2client_dh_group[ data['ike_encryption']['dh_group'] ]
+
 print('\n\n==== <snip> ====')
 if args.os == 'ios':
     print(render_to_string('ipsec/ios_profile.j2', data))
-- 
cgit v1.2.3