From 5d9183aa13df5037e7cdf588a0cdb71c1d5d4d4b Mon Sep 17 00:00:00 2001 From: Pascal Mathis Date: Sun, 25 Aug 2019 21:44:26 +0200 Subject: [PATCH 1/3] ipsec: Add support for public key authentication The current IPsec plugin implementation does not support public key authentication, which allows for a more secure mutual authentication than PSK while still not introducing the complexity of X509 certificates. The authentication can easily be set up by generating a bare RSA keypair chain on both machines, followed by exchanging the public keys between the two peers. This commit introduces public key authentication functionality by adding a new authentication method to phase 1 configuration called "Mutual Public Key" and adding a menu entry "Key Pairs", which allows adding public keys + optional private keys. It was successfully tested against a Linux virtual machine running Strongswan 5 and the entered RSA keys are automatically verified for correctness. Useful commands for generating a bare RSA keypair: $ ipsec pki --gen --type rsa --outform pem --size 4096 > private.pem $ ipsec pki --pub --outform pem --in private.pem > public.pem Signed-off-by: Pascal Mathis --- src/etc/inc/plugins.inc.d/ipsec.inc | 82 +++++++- .../OPNsense/IPsec/Api/KeyPairsController.php | 114 +++++++++++ .../IPsec/Api/LegacySubsystemController.php | 79 ++++++++ .../OPNsense/IPsec/KeyPairsController.php | 41 ++++ .../OPNsense/IPsec/forms/dialogKeyPair.xml | 33 +++ .../mvc/app/models/OPNsense/IPsec/ACL/ACL.xml | 11 + .../mvc/app/models/OPNsense/IPsec/IPsec.php | 191 ++++++++++++++++++ .../mvc/app/models/OPNsense/IPsec/IPsec.xml | 30 +++ .../app/models/OPNsense/IPsec/Menu/Menu.xml | 7 + .../app/views/OPNsense/IPsec/key_pairs.volt | 123 +++++++++++ .../service/conf/actions.d/actions_ipsec.conf | 6 + src/www/vpn_ipsec_phase1.php | 69 ++++++- 12 files changed, 779 insertions(+), 7 deletions(-) create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/KeyPairsController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/LegacySubsystemController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/KeyPairsController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogKeyPair.xml create mode 100644 src/opnsense/mvc/app/models/OPNsense/IPsec/ACL/ACL.xml create mode 100644 src/opnsense/mvc/app/models/OPNsense/IPsec/IPsec.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/IPsec/IPsec.xml create mode 100644 src/opnsense/mvc/app/models/OPNsense/IPsec/Menu/Menu.xml create mode 100644 src/opnsense/mvc/app/views/OPNsense/IPsec/key_pairs.volt diff --git a/src/etc/inc/plugins.inc.d/ipsec.inc b/src/etc/inc/plugins.inc.d/ipsec.inc index 204de0ae9..31c409be6 100644 --- a/src/etc/inc/plugins.inc.d/ipsec.inc +++ b/src/etc/inc/plugins.inc.d/ipsec.inc @@ -1,6 +1,7 @@ * Copyright (C) 2016 Deciso B.V. * Copyright (C) 2008 Shrew Soft Inc. * Copyright (C) 2008 Ermal Luçi @@ -86,6 +87,7 @@ function ipsec_p1_authentication_methods() 'rsa_eap-mschapv2' => array( 'name' => 'Mutual RSA + EAP-MSCHAPV2', 'mobile' => true), 'eap-radius' => array( 'name' => 'EAP-RADIUS', 'mobile' => true), 'rsasig' => array( 'name' => 'Mutual RSA', 'mobile' => false ), + 'pubkey' => array( 'name' => 'Mutual Public Key', 'mobile' => false ), 'pre_shared_key' => array( 'name' => 'Mutual PSK', 'mobile' => false ), ); } @@ -542,6 +544,14 @@ function ipsec_mobilekey_sort() }); } +function ipsec_lookup_keypair($uuid) +{ + $mdl = new \OPNsense\IPsec\IPsec(); + $node = $mdl->getNodeByReference('keyPairs.keyPair.' . $uuid); + + return $node ? $node->getNodes() : null; +} + function ipsec_get_number_of_phase2($ikeid) { global $config; @@ -791,14 +801,16 @@ function ipsec_configure_do($verbose = false, $interface = '') } else { $certpath = "/usr/local/etc/ipsec.d/certs"; $capath = "/usr/local/etc/ipsec.d/cacerts"; - $keypath = "/usr/local/etc/ipsec.d/private"; + $publickeypath = "/usr/local/etc/ipsec.d/public"; + $privatekeypath = "/usr/local/etc/ipsec.d/private"; mwexec("/sbin/ifconfig enc0 up"); set_single_sysctl("net.inet.ip.ipsec_in_use", "1"); /* needed directories for config files */ @mkdir($capath); - @mkdir($keypath); + @mkdir($privatekeypath); + @mkdir($publickeypath); @mkdir($certpath); @mkdir('/usr/local/etc/ipsec.d'); @mkdir('/usr/local/etc/ipsec.d/crls'); @@ -1093,7 +1105,7 @@ function ipsec_configure_do($verbose = false, $interface = '') @chmod($certpath, 0600); - $ph1keyfile = "{$keypath}/cert-{$ph1ent['ikeid']}.key"; + $ph1keyfile = "{$privatekeypath}/cert-{$ph1ent['ikeid']}.key"; if (!file_put_contents($ph1keyfile, base64_decode($cert['prv']))) { log_error(sprintf('Error: Cannot write phase1 key file for %s', $ph1ent['name'])); continue; @@ -1120,6 +1132,59 @@ function ipsec_configure_do($verbose = false, $interface = '') } } + /* Generate files for key pairs (e.g. RSA) */ + foreach ($a_phase1 as $ph1ent) { + if(isset($ph1ent['disabled'])) { + continue; + } + + if (!empty($ph1ent['local-kpref'])) { + $keyPair = ipsec_lookup_keypair($ph1ent['local-kpref']); + if (!$keyPair || empty($keyPair['publicKey']) || empty($keyPair['privateKey'])) { + log_error(sprintf('Error: Invalid phase1 local key pair reference for %s', $ph1ent['name'])); + continue; + } + + @chmod($publickeypath, 0600); + + $ph1publickeyfile = "${publickeypath}/publickey-local-{$ph1ent['ikeid']}.pem"; + if (!file_put_contents($ph1publickeyfile, $keyPair['publicKey'])) { + log_error(sprintf('Error: Cannot write phase1 local public key file for %s', $ph1ent['name'])); + @unlink($ph1publickeyfile); + continue; + } + @chmod($ph1publickeyfile, 0600); + + $ph1privatekeyfile = "${privatekeypath}/privatekey-local-{$ph1ent['ikeid']}.pem"; + if (!file_put_contents($ph1privatekeyfile, $keyPair['privateKey'])) { + log_error(sprintf('Error: Cannot write phase1 local private key file for %s', $ph1ent['name'])); + @unlink($ph1privatekeyfile); + continue; + } + @chmod($ph1privatekeyfile, 0600); + + $pskconf .= " : RSA {$ph1privatekeyfile}\n"; + } + + if (!empty($ph1ent['peer-kpref'])) { + $keyPair = ipsec_lookup_keypair($ph1ent['peer-kpref']); + if (!$keyPair || empty($keyPair['publicKey'])) { + log_error(sprintf('Error: Invalid phase1 peer key pair reference for %s', $ph1ent['name'])); + continue; + } + + @chmod($publickeypath, 0600); + + $ph1publickeyfile = "${publickeypath}/publickey-peer-{$ph1ent['ikeid']}.pem"; + if (!file_put_contents($ph1publickeyfile, $keyPair['publicKey'])) { + log_error(sprintf('Error: Cannot write phase1 peer public key file for %s', $ph1ent['name'])); + @unlink($ph1publickeyfile); + continue; + } + @chmod($ph1publickeyfile, 0600); + } + } + /* Add user PSKs */ if (isset($config['system']['user']) && is_array($config['system']['user'])) { foreach ($config['system']['user'] as $user) { @@ -1289,12 +1354,14 @@ function ipsec_configure_do($verbose = false, $interface = '') $authentication = "leftauth = psk\n\trightauth = psk"; break; case 'rsasig': + case 'pubkey': $authentication = "leftauth = pubkey\n\trightauth = pubkey"; break; case 'hybrid_rsa_server': $authentication = "leftauth = pubkey\n\trightauth = xauth"; break; } + if (!empty($ph1ent['certref'])) { $authentication .= "\n\tleftcert = {$certpath}/cert-{$ph1ent['ikeid']}.crt"; $authentication .= "\n\tleftsendcert = always"; @@ -1309,8 +1376,15 @@ function ipsec_configure_do($verbose = false, $interface = '') $authentication .= "\n\trightca = \"/$rightca\""; } } - $left_spec = $ep; + if (!empty($ph1ent['local-kpref'])) { + $authentication .= "\n\tleftsigkey = {$publickeypath}/publickey-local-{$ph1ent['ikeid']}.pem"; + } + if (!empty($ph1ent['peer-kpref'])) { + $authentication .= "\n\trightsigkey = {$publickeypath}/publickey-peer-{$ph1ent['ikeid']}.pem"; + } + + $left_spec = $ep; if (isset($ph1ent['reauth_enable'])) { $reauth = "reauth = no"; } else { diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/KeyPairsController.php b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/KeyPairsController.php new file mode 100644 index 000000000..94b3f6548 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/KeyPairsController.php @@ -0,0 +1,114 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +namespace OPNsense\IPsec\Api; + +use OPNsense\Base\ApiMutableModelControllerBase; + +/** + * Class KeyPairsController + * @package OPNsense\IPsec\Api + */ +class KeyPairsController extends ApiMutableModelControllerBase +{ + protected static $internalModelName = 'ipsec'; + protected static $internalModelClass = 'OPNsense\IPsec\IPsec'; + + /** + * Search key pairs + * @return array + * @throws \ReflectionException + */ + public function searchItemAction() + { + return $this->searchBase( + 'keyPairs.keyPair', + ['name', 'keyType', 'keySize', 'keyFingerprint'] + ); + } + + /** + * Update key pair with given properties + * @param $uuid + * @return array + * @throws \OPNsense\Base\UserException + * @throws \ReflectionException + */ + public function setItemAction($uuid = null) + { + $response = $this->setBase('keyPair', 'keyPairs.keyPair', $uuid); + if (!empty($response['result']) && $response['result'] === 'saved') { + touch('/tmp/ipsec.dirty'); // mark_subsystem_dirty('ipsec') + } + + return $response; + } + + /** + * Add new key pair with given properties + * @return array + * @throws \OPNsense\Base\UserException + * @throws \ReflectionException + */ + public function addItemAction() + { + $response = $this->addBase('keyPair', 'keyPairs.keyPair'); + if (!empty($response['result']) && $response['result'] === 'saved') { + touch('/tmp/ipsec.dirty'); // mark_subsystem_dirty('ipsec') + } + + return $response; + } + + /** + * Retrieve key pair or return defaults for new one + * @param $uuid + * @return array + * @throws \ReflectionException + */ + public function getItemAction($uuid = null) + { + return $this->getBase('keyPair', 'keyPairs.keyPair', $uuid); + } + + /** + * Delete key pair by UUID + * @param $uuid + * @return array + * @throws \OPNsense\Base\UserException + * @throws \ReflectionException + */ + public function delItemAction($uuid) + { + $response = $this->delBase('keyPairs.keyPair', $uuid); + if (!empty($response['result']) && $response['result'] === 'deleted') { + touch('/tmp/ipsec.dirty'); // mark_subsystem_dirty('ipsec') + } + + return $response; + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/LegacySubsystemController.php b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/LegacySubsystemController.php new file mode 100644 index 000000000..fa5d53e39 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/LegacySubsystemController.php @@ -0,0 +1,79 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +namespace OPNsense\IPsec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\Core\Backend; + +/** + * Class LegacySubsystemController + * @package OPNsense\IPsec\Api + */ +class LegacySubsystemController extends ApiControllerBase +{ + /** + * Returns the status of the legacy subsystem, which currently only includes a boolean specifying if the subsystem + * is marked as dirty, which means that there are pending changes. + * @return array + */ + public function statusAction() + { + return [ + 'isDirty' => file_exists('/tmp/ipsec.dirty') // is_subsystem_dirty('ipsec') + ]; + } + + /** + * Apply the IPsec configuration using the legacy subsystem and return a message describing the result + * @return array + * @throws \Exception + */ + public function applyConfigAction() + { + try { + if (!$this->request->isPost()) + throw new \Exception(gettext('Request method not allowed, expected POST')); + + $backend = new Backend(); + $bckresult = trim($backend->configdRun('ipsec reconfigure')); + if ($bckresult !== 'OK') + throw new \Exception($bckresult); + + // clear_subsystem_dirty('ipsec') + if (!@unlink('/tmp/ipsec.dirty')) + throw new \Exception(gettext('Could not remove /tmp/ipsec.dirty to mark subsystem as clean')); + + return ['message' => gettext('The changes have been applied successfully.')]; + } catch (\Exception $e) { + throw new \Exception(sprintf( + gettext('Unable to apply IPsec subsystem configuration: %s'), + $e->getMessage() + )); + } + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/KeyPairsController.php b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/KeyPairsController.php new file mode 100644 index 000000000..207937cd0 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/KeyPairsController.php @@ -0,0 +1,41 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +namespace OPNsense\IPsec; + +/** + * Class KeyPairsController + * @package OPNsense\IPsec + */ +class KeyPairsController extends \OPNsense\Base\IndexController +{ + public function indexAction() + { + $this->view->formDialogKeyPair = $this->getForm('dialogKeyPair'); + $this->view->pick('OPNsense/IPsec/key_pairs'); + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogKeyPair.xml b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogKeyPair.xml new file mode 100644 index 000000000..e0992aeb8 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogKeyPair.xml @@ -0,0 +1,33 @@ +
+ + keyPair.name + + text + Enter a name for this key pair. The name should help you to identify this key pair. + + + keyPair.keyType + + dropdown + Select the type of the key pair. Currently RSA is the only supported type. + + + keyPair.publicKey + + textbox + + Paste a public key of the selected type in X.509 format. + Must be pasted including the '-----BEGIN ...-----' and '-----END [...]-----' headers. + + + + keyPair.privateKey + + textbox + + Paste an optional private key of the selected type in X.509 format which belongs to the public key specified above. + When adding the public key of a peer, this field should not be specified as the private key remains solely with the peer. + Must be pasted including the '-----BEGIN ...-----' and '-----END [...]-----' headers. + + +
diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/ACL/ACL.xml b/src/opnsense/mvc/app/models/OPNsense/IPsec/ACL/ACL.xml new file mode 100644 index 000000000..771b7c2fb --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/ACL/ACL.xml @@ -0,0 +1,11 @@ + + + VPN: IPsec: Key Pairs + Allow access to the IPsec Key Pairs + + ui/ipsec/key-pairs/* + api/ipsec/key-pairs/* + api/ipsec/legacy-subsystem/* + + + diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/IPsec.php b/src/opnsense/mvc/app/models/OPNsense/IPsec/IPsec.php new file mode 100644 index 000000000..a67b74cb1 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/IPsec.php @@ -0,0 +1,191 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +namespace OPNsense\IPsec; + +use OPNsense\Base\BaseModel; + +/** + * Class IPsec + * @package OPNsense\IPsec + */ +class IPsec extends BaseModel +{ + /** + * {@inheritdoc} + */ + public function performValidation($validateFullModel = false) + { + $messages = parent::performValidation($validateFullModel); + $keyPairs = []; + + foreach ($this->getFlatNodes() as $key => $node) { + if ($validateFullModel || $node->isFieldChanged()) { + $tagName = $node->getInternalXMLTagName(); + $parentNode = $node->getParentNode(); + $parentKey = $parentNode->__reference; + $parentTagName = $parentNode->getInternalXMLTagName(); + + if ($parentTagName === 'keyPair' && in_array($tagName, ['keyType', 'privateKey', 'publicKey'])) { + $keyPairs[$parentKey] = $parentNode; + } + } + } + + foreach ($keyPairs as $key => $node) { + $this->validateKeyPair($key, $node, $messages); + } + + return $messages; + } + + /** + * Validates a keyPair instance within a model. This method does change the model contents by replacing the public + * and private key contents with a sanitized representation as well as storing the key size and fingerprint. + * @param $nodeKey string Fully-qualified key of the keyPair instance within a model + * @param $keyPair \OPNsense\Base\FieldTypes\BaseField Field instance of a keyPair + * @param $messages \Phalcon\Validation\Message\Group Validation message group + */ + private function validateKeyPair($nodeKey, $keyPair, $messages) + { + $publicKey = $privateKey = null; + if (empty((string)$keyPair->keyType)) + return; + + // Validate public key + if (!empty((string)$keyPair->publicKey)) { + try { + $publicKey = $this->parseCryptographicKey( + (string)$keyPair->publicKey, + (string)$keyPair->keyType . '-public' + ); + } catch (\Exception $e) { + $messages->appendMessage(new \Phalcon\Validation\Message($e->getMessage(), $nodeKey . '.publicKey')); + } + } + + // Validate private key + if (!empty((string)$keyPair->privateKey)) { + try { + $privateKey = $this->parseCryptographicKey( + (string)$keyPair->privateKey, + (string)$keyPair->keyType . '-private' + ); + } catch (\Exception $e) { + $messages->appendMessage(new \Phalcon\Validation\Message($e->getMessage(), $nodeKey . '.privateKey')); + } + } + + // Compare SHA1 fingerprint of public and private keys to check if they belong to each other + if ($publicKey && $privateKey) { + if ($publicKey['fingerprint'] !== $privateKey['fingerprint']) { + $messages->appendMessage(new \Phalcon\Validation\Message( + gettext('This private key does not belong to the given public key.'), $nodeKey . '.privateKey' + )); + } + } + + // Store sanitized representation of keys and cache key statistics + $keyPair->publicKey = $publicKey ? $publicKey['pem'] : (string)$keyPair->publicKey; + $keyPair->privateKey = $privateKey ? $privateKey['pem'] : (string)$keyPair->privateKey; + $keyPair->keySize = $publicKey ? $publicKey['size'] : 0; + $keyPair->keyFingerprint = $publicKey ? $publicKey['fingerprint'] : ''; + } + + /** + * Parse a cryptographic key of a given type using OpenSSL and return an array of informational data. + * @param $keyString string + * @param $keyType string + * @return array + */ + public function parseCryptographicKey($keyString, $keyType) + { + // Attempt to load key with correct type + if ($keyType === 'rsa-public') { + $key = openssl_pkey_get_public($keyString); + } elseif ($keyType === 'rsa-private') { + $key = openssl_pkey_get_private($keyString); + } else { + throw new \InvalidArgumentException(sprintf( + gettext('Unsupported key type: %s'), + $keyType + )); + } + + // Ensure that key has been successfully loaded + if ($key === false) { + throw new \InvalidArgumentException(sprintf( + gettext('Could not load potentially invalid %s key: %s'), + $keyType, openssl_error_string() + )); + } + + // Attempt to fetch key details + $keyDetails = openssl_pkey_get_details($key); + if ($keyDetails === false) { + throw new \RuntimeException(sprintf( + gettext('Could not fetch details for %s key: %s'), + $keyType, openssl_error_string() + )); + } + + // Verify given public key is valid for usage with Strongswan + if ($keyDetails['type'] !== OPENSSL_KEYTYPE_RSA) { + throw new \InvalidArgumentException(sprintf( + gettext('Unsupported OpenSSL key type [%d] for %s key, expected RSA.'), + $keyDetails['type'], $keyType + )); + } + + // Fetch sanitized PEM representation of key + if ($keyType === 'rsa-private') { + if (!openssl_pkey_export($key, $keySanitized, null)) { + throw new \RuntimeException(sprintf( + gettext('Could not generate sanitized %s key in PEM format: %s'), + $keyType, openssl_error_string() + )); + } + } else { + $keySanitized = $keyDetails['key']; + } + + // Calculate fingerprint for the public key (when a private key was given, its public key is calculated) + $keyUnwrapped = trim(preg_replace('/\\+s/', '', preg_replace( + '~^-----BEGIN(?:[A-Z]+ )? PUBLIC KEY-----([A-Za-z0-9+/=\\s]+)-----END(?:[A-Z]+ )? PUBLIC KEY-----$~m', + '\\1', $keyDetails['key'] + ))); + $keyFingerprint = substr(chunk_split(hash('sha1', base64_decode($keyUnwrapped)), 2, ':'), 0, -1); + + return [ + 'resource' => $key, + 'size' => $keyDetails['bits'], + 'fingerprint' => $keyFingerprint, + 'type' => $keyType, + 'pem' => $keySanitized + ]; + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/IPsec.xml b/src/opnsense/mvc/app/models/OPNsense/IPsec/IPsec.xml new file mode 100644 index 000000000..683369fdb --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/IPsec.xml @@ -0,0 +1,30 @@ + + //OPNsense/IPsec + + OPNsense IPsec + + + + + + Y + + + Y + rsa + + RSA + + + + Y + + + N + + + + + + + diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/IPsec/Menu/Menu.xml new file mode 100644 index 000000000..e66d480d5 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/Menu/Menu.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/opnsense/mvc/app/views/OPNsense/IPsec/key_pairs.volt b/src/opnsense/mvc/app/views/OPNsense/IPsec/key_pairs.volt new file mode 100644 index 000000000..2091ceeb1 --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/IPsec/key_pairs.volt @@ -0,0 +1,123 @@ +{# +Copyright (C) 2019 Pascal Mathis +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +#} + + + + + + + + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Name') }}{{ lang._('Key Type') }}{{ lang._('Key Size') }}{{ lang._('Key Fingerprint') }}{{ lang._('Commands') }}
+ + +
+ +{{ partial("layout_partials/base_dialog",['fields':formDialogKeyPair,'id':'DialogKeyPair','label':lang._('Edit key pair')]) }} diff --git a/src/opnsense/service/conf/actions.d/actions_ipsec.conf b/src/opnsense/service/conf/actions.d/actions_ipsec.conf index b069a6b4e..c6298273f 100644 --- a/src/opnsense/service/conf/actions.d/actions_ipsec.conf +++ b/src/opnsense/service/conf/actions.d/actions_ipsec.conf @@ -39,3 +39,9 @@ command:/usr/local/sbin/ipsec restart parameters: type=script message:IPsec service restart + +[reconfigure] +command:/usr/local/sbin/pluginctl -c ipsec +parameters: +type:script +message:IPsec config generation diff --git a/src/www/vpn_ipsec_phase1.php b/src/www/vpn_ipsec_phase1.php index 934af33e1..2b8ea5788 100644 --- a/src/www/vpn_ipsec_phase1.php +++ b/src/www/vpn_ipsec_phase1.php @@ -1,6 +1,7 @@ * Copyright (C) 2014-2015 Deciso B.V. * Copyright (C) 2008 Shrew Soft Inc. * Copyright (C) 2003-2005 Manuel Kasper @@ -62,6 +63,14 @@ function ipsec_ikeid_next() { return $ikeid; } +function ipsec_keypairs() +{ + $mdl = new \OPNsense\IPsec\IPsec(); + $node = $mdl->getNodeByReference('keyPairs.keyPair'); + + return $node ? $node->getNodes() : []; +} + config_read_array('ipsec', 'phase1'); config_read_array('ipsec', 'phase2'); @@ -80,7 +89,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { $phase1_fields = "mode,protocol,myid_type,myid_data,peerid_type,peerid_data ,encryption-algorithm,lifetime,authentication_method,descr,nat_traversal,rightallowany ,interface,iketype,dpd_delay,dpd_maxfail,remote-gateway,pre-shared-key,certref - ,caref,reauth_enable,rekey_enable,auto,tunnel_isolation,authservers,mobike"; + ,caref,local-kpref,peer-kpref,reauth_enable,rekey_enable,auto,tunnel_isolation,authservers,mobike"; if (isset($p1index) && isset($config['ipsec']['phase1'][$p1index])) { // 1-on-1 copy foreach (explode(",", $phase1_fields) as $fieldname) { @@ -211,6 +220,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { $reqdfields = explode(" ", "caref certref"); $reqdfieldsn = array(gettext("Certificate Authority"),gettext("Certificate")); break; + case "pubkey": + $reqdfields = explode(" ", "local-kpref peer-kpref"); + $reqdfieldsn = array(gettext("Local Key Pair"),gettext("Peer Key Pair")); + break; } if (empty($pconfig['mobile'])) { @@ -350,7 +363,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { if (count($input_errors) == 0) { $copy_fields = "ikeid,iketype,interface,mode,protocol,myid_type,myid_data ,peerid_type,peerid_data,encryption-algorithm, - ,lifetime,pre-shared-key,certref,caref,authentication_method,descr + ,lifetime,pre-shared-key,certref,caref,authentication_method,descr,local-kpref,peer-kpref ,nat_traversal,auto,mobike"; foreach (explode(",",$copy_fields) as $fieldname) { @@ -512,6 +525,10 @@ include("head.inc"); $(".auth_eap_tls").show(); $(".auth_eap_tls :input").prop( "disabled", false ); break; + case "pubkey": + $(".auth_pubkey").show(); + $(".auth_pubkey :input").prop("disabled", false); + break; default: /* psk modes*/ $(".auth_psk").show(); $(".auth_psk :input").prop( "disabled", false ); @@ -792,7 +809,7 @@ endforeach; ?> - + + $keypair) : + if ($keypair['publicKey'] and $keypair['privateKey']) : + ?> + + + + + + + + + + + + + From 8227a0cbca6d5bd01442cdddae465998f95f89e9 Mon Sep 17 00:00:00 2001 From: Pascal Mathis Date: Sun, 1 Sep 2019 11:47:03 +0200 Subject: [PATCH 2/3] ipsec: Move menu and ACL entries into MVC code This commit moves all menu and ACL entries from the legacy code of the IPsec subsystem into the new MVC codebase. Additionally, a small bug in the current master of OPNsense has been fixed, where the ACL "page-status-systemlogs-ppp" has been mistakenly labeled as "IPsec VPN" instead of "PPP". Signed-off-by: Pascal Mathis --- .../mvc/app/models/OPNsense/Core/ACL/ACL.xml | 68 +----------------- .../app/models/OPNsense/Core/Menu/Menu.xml | 20 ------ .../mvc/app/models/OPNsense/IPsec/ACL/ACL.xml | 69 +++++++++++++++++++ .../app/models/OPNsense/IPsec/Menu/Menu.xml | 22 +++++- 4 files changed, 90 insertions(+), 89 deletions(-) diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml index cca322db3..099e36d16 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml @@ -506,30 +506,6 @@ status_interfaces.php* - - Status: IPsec - - diag_ipsec.php* - - - - Status: IPsec: Leasespage - - diag_ipsec_leases.php* - - - - Status: IPsec: SAD - - diag_ipsec_sad.php* - - - - Status: IPsec: SPD - - diag_ipsec_spd.php* - - Status: OpenVPN @@ -548,14 +524,8 @@ diag_logs_auth.php* - - Status: System logs: IPsec VPN - - diag_logs_ipsec.php* - - - Status: System logs: IPsec VPN + Status: System logs: PPP diag_logs_ppp.php* @@ -733,42 +703,6 @@ system_usermanager_passwordmg.php* - - VPN: IPsec - - vpn_ipsec.php* - - - - VPN: IPsec: Edit Phase 1 - - vpn_ipsec_phase1.php* - - - - VPN: IPsec: Edit Phase 2 - - vpn_ipsec_phase2.php* - - - - VPN: IPsec: Edit Pre-Shared Keys - - vpn_ipsec_keys_edit.php* - - - - VPN: IPsec: Mobile - - vpn_ipsec_mobile.php* - - - - VPN: IPsec: Pre-Shared Keys List - - vpn_ipsec_keys.php* - - VPN: OpenVPN: Client Export Utility diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml index f12e49bfe..8ceb0844e 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml @@ -217,26 +217,6 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/ACL/ACL.xml b/src/opnsense/mvc/app/models/OPNsense/IPsec/ACL/ACL.xml index 771b7c2fb..660c00a78 100644 --- a/src/opnsense/mvc/app/models/OPNsense/IPsec/ACL/ACL.xml +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/ACL/ACL.xml @@ -1,4 +1,5 @@ + VPN: IPsec: Key Pairs Allow access to the IPsec Key Pairs @@ -8,4 +9,72 @@ api/ipsec/legacy-subsystem/* + + + + VPN: IPsec + + vpn_ipsec.php* + + + + VPN: IPsec: Edit Phase 1 + + vpn_ipsec_phase1.php* + + + + VPN: IPsec: Edit Phase 2 + + vpn_ipsec_phase2.php* + + + + VPN: IPsec: Edit Pre-Shared Keys + + vpn_ipsec_keys_edit.php* + + + + VPN: IPsec: Mobile + + vpn_ipsec_mobile.php* + + + + VPN: IPsec: Pre-Shared Keys List + + vpn_ipsec_keys.php* + + + + Status: IPsec + + diag_ipsec.php* + + + + Status: IPsec: Leasespage + + diag_ipsec_leases.php* + + + + Status: IPsec: SAD + + diag_ipsec_sad.php* + + + + Status: IPsec: SPD + + diag_ipsec_spd.php* + + + + Status: System logs: IPsec VPN + + diag_logs_ipsec.php* + + diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/IPsec/Menu/Menu.xml index e66d480d5..6bc0e6a24 100644 --- a/src/opnsense/mvc/app/models/OPNsense/IPsec/Menu/Menu.xml +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/Menu/Menu.xml @@ -1,7 +1,25 @@ - - + + + + + + + + + + + + + + + + + + + + From 013e802abfccf00833e47edc278f5c52a10953bf Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Fri, 13 Sep 2019 14:17:33 +0200 Subject: [PATCH 3/3] IPSec public key authentication, fix background on /ui/ipsec/key-pairs --- .../app/views/OPNsense/IPsec/key_pairs.volt | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/opnsense/mvc/app/views/OPNsense/IPsec/key_pairs.volt b/src/opnsense/mvc/app/views/OPNsense/IPsec/key_pairs.volt index 2091ceeb1..4868c9f37 100644 --- a/src/opnsense/mvc/app/views/OPNsense/IPsec/key_pairs.volt +++ b/src/opnsense/mvc/app/views/OPNsense/IPsec/key_pairs.volt @@ -91,33 +91,34 @@ POSSIBILITY OF SUCH DAMAGE. {{ lang._('You must apply the changes in order for them to take effect.') }} - - - - - - - - - - - - - - - - - - - -
{{ lang._('ID') }}{{ lang._('Name') }}{{ lang._('Key Type') }}{{ lang._('Key Size') }}{{ lang._('Key Fingerprint') }}{{ lang._('Commands') }}
- - -
+
+ + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Name') }}{{ lang._('Key Type') }}{{ lang._('Key Size') }}{{ lang._('Key Fingerprint') }}{{ lang._('Commands') }}
+ + +
+
{{ partial("layout_partials/base_dialog",['fields':formDialogKeyPair,'id':'DialogKeyPair','label':lang._('Edit key pair')]) }}