Merge branch 'ppmathis-feature/ipsec-pubkey-auth'

This commit is contained in:
Ad Schellevis 2019-09-13 14:18:35 +02:00
commit 6b542e91d0
14 changed files with 868 additions and 94 deletions

View File

@ -1,6 +1,7 @@
<?php
/*
* Copyright (C) 2019 Pascal Mathis <mail@pascalmathis.com>
* Copyright (C) 2016 Deciso B.V.
* Copyright (C) 2008 Shrew Soft Inc. <mgrooms@shrew.net>
* 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 {

View File

@ -0,0 +1,114 @@
<?php
/*
* Copyright (C) 2019 Pascal Mathis <mail@pascalmathis.com>
* 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;
}
}

View File

@ -0,0 +1,79 @@
<?php
/*
* Copyright (C) 2019 Pascal Mathis <mail@pascalmathis.com>
* 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()
));
}
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* Copyright (C) 2019 Pascal Mathis <mail@pascalmathis.com>
* 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');
}
}

View File

@ -0,0 +1,33 @@
<form>
<field>
<id>keyPair.name</id>
<label>Name</label>
<type>text</type>
<help>Enter a name for this key pair. The name should help you to identify this key pair.</help>
</field>
<field>
<id>keyPair.keyType</id>
<label>Key Type</label>
<type>dropdown</type>
<help>Select the type of the key pair. Currently RSA is the only supported type.</help>
</field>
<field>
<id>keyPair.publicKey</id>
<label>Public Key</label>
<type>textbox</type>
<help>
Paste a public key of the selected type in X.509 format.
Must be pasted including the '-----BEGIN ...-----' and '-----END [...]-----' headers.
</help>
</field>
<field>
<id>keyPair.privateKey</id>
<label>Private Key</label>
<type>textbox</type>
<help>
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.
</help>
</field>
</form>

View File

@ -506,30 +506,6 @@
<pattern>status_interfaces.php*</pattern>
</patterns>
</page-status-interfaces>
<page-status-ipsec>
<name>Status: IPsec</name>
<patterns>
<pattern>diag_ipsec.php*</pattern>
</patterns>
</page-status-ipsec>
<page-status-ipsec-leases>
<name>Status: IPsec: Leasespage</name>
<patterns>
<pattern>diag_ipsec_leases.php*</pattern>
</patterns>
</page-status-ipsec-leases>
<page-status-ipsec-sad>
<name>Status: IPsec: SAD</name>
<patterns>
<pattern>diag_ipsec_sad.php*</pattern>
</patterns>
</page-status-ipsec-sad>
<page-status-ipsec-spd>
<name>Status: IPsec: SPD</name>
<patterns>
<pattern>diag_ipsec_spd.php*</pattern>
</patterns>
</page-status-ipsec-spd>
<page-status-openvpn>
<name>Status: OpenVPN</name>
<patterns>
@ -548,14 +524,8 @@
<pattern>diag_logs_auth.php*</pattern>
</patterns>
</page-status-systemlogs-portalauth>
<page-status-systemlogs-ipsecvpn>
<name>Status: System logs: IPsec VPN</name>
<patterns>
<pattern>diag_logs_ipsec.php*</pattern>
</patterns>
</page-status-systemlogs-ipsecvpn>
<page-status-systemlogs-ppp>
<name>Status: System logs: IPsec VPN</name>
<name>Status: System logs: PPP</name>
<patterns>
<pattern>diag_logs_ppp.php*</pattern>
</patterns>
@ -733,42 +703,6 @@
<pattern>system_usermanager_passwordmg.php*</pattern>
</patterns>
</page-system-usermanager-passwordmg>
<page-vpn-ipsec>
<name>VPN: IPsec</name>
<patterns>
<pattern>vpn_ipsec.php*</pattern>
</patterns>
</page-vpn-ipsec>
<page-vpn-ipsec-editphase1>
<name>VPN: IPsec: Edit Phase 1</name>
<patterns>
<pattern>vpn_ipsec_phase1.php*</pattern>
</patterns>
</page-vpn-ipsec-editphase1>
<page-vpn-ipsec-editphase2>
<name>VPN: IPsec: Edit Phase 2</name>
<patterns>
<pattern>vpn_ipsec_phase2.php*</pattern>
</patterns>
</page-vpn-ipsec-editphase2>
<page-vpn-ipsec-editkeys>
<name>VPN: IPsec: Edit Pre-Shared Keys</name>
<patterns>
<pattern>vpn_ipsec_keys_edit.php*</pattern>
</patterns>
</page-vpn-ipsec-editkeys>
<page-vpn-ipsec-mobile>
<name>VPN: IPsec: Mobile</name>
<patterns>
<pattern>vpn_ipsec_mobile.php*</pattern>
</patterns>
</page-vpn-ipsec-mobile>
<page-vpn-ipsec-listkeys>
<name>VPN: IPsec: Pre-Shared Keys List</name>
<patterns>
<pattern>vpn_ipsec_keys.php*</pattern>
</patterns>
</page-vpn-ipsec-listkeys>
<page-openvpn-client-export>
<name>VPN: OpenVPN: Client Export Utility</name>
<patterns>

View File

@ -217,26 +217,6 @@
<RouterAdv VisibleName="Router Advertisements" cssClass="fa fa-bullseye fa-fw" />
</Services>
<VPN order="50" cssClass="fa fa-globe">
<IPsec cssClass="fa fa-lock fa-fw" order="10">
<Tunnels order="10" VisibleName="Tunnel Settings" url="/vpn_ipsec.php">
<Phase1 url="/vpn_ipsec_phase1.php*" visibility="hidden"/>
<Phase2 url="/vpn_ipsec_phase2.php*" visibility="hidden"/>
</Tunnels>
<Mobile order="20" VisibleName="Mobile Clients" url="/vpn_ipsec_mobile.php">
<Act url="/vpn_ipsec_mobile.php*" visibility="hidden"/>
</Mobile>
<Keys order="30" VisibleName="Pre-Shared Keys" url="/vpn_ipsec_keys.php">
<Edit url="/vpn_ipsec_keys_edit.php*" visibility="hidden"/>
</Keys>
<Settings order="40" VisibleName="Advanced Settings" url="/vpn_ipsec_settings.php"/>
<Status order="50" VisibleName="Status Overview" url="/diag_ipsec.php">
<Act url="/diag_ipsec.php?*" visibility="hidden"/>
</Status>
<Leases order="60" VisibleName="Lease Status" url="/diag_ipsec_leases.php"/>
<SAD order="70" VisibleName="Security Association Database" url="/diag_ipsec_sad.php"/>
<SPD order="80" VisibleName="Security Policy Database" url="/diag_ipsec_spd.php"/>
<LogFile order="90" VisibleName="Log File" url="/diag_logs_ipsec.php"/>
</IPsec>
<OpenVPN cssClass="fa fa-lock fa-fw" order="20">
<Servers order="10" url="/vpn_openvpn_server.php">
<Edit url="/vpn_openvpn_server.php?*" visibility="hidden"/>

View File

@ -0,0 +1,80 @@
<acl>
<!-- ACLs for MVC code -->
<page-vpn-ipsec-keypairs>
<name>VPN: IPsec: Key Pairs</name>
<description>Allow access to the IPsec Key Pairs</description>
<patterns>
<pattern>ui/ipsec/key-pairs/*</pattern>
<pattern>api/ipsec/key-pairs/*</pattern>
<pattern>api/ipsec/legacy-subsystem/*</pattern>
</patterns>
</page-vpn-ipsec-keypairs>
<!-- ACLs for legacy code -->
<page-vpn-ipsec>
<name>VPN: IPsec</name>
<patterns>
<pattern>vpn_ipsec.php*</pattern>
</patterns>
</page-vpn-ipsec>
<page-vpn-ipsec-editphase1>
<name>VPN: IPsec: Edit Phase 1</name>
<patterns>
<pattern>vpn_ipsec_phase1.php*</pattern>
</patterns>
</page-vpn-ipsec-editphase1>
<page-vpn-ipsec-editphase2>
<name>VPN: IPsec: Edit Phase 2</name>
<patterns>
<pattern>vpn_ipsec_phase2.php*</pattern>
</patterns>
</page-vpn-ipsec-editphase2>
<page-vpn-ipsec-editkeys>
<name>VPN: IPsec: Edit Pre-Shared Keys</name>
<patterns>
<pattern>vpn_ipsec_keys_edit.php*</pattern>
</patterns>
</page-vpn-ipsec-editkeys>
<page-vpn-ipsec-mobile>
<name>VPN: IPsec: Mobile</name>
<patterns>
<pattern>vpn_ipsec_mobile.php*</pattern>
</patterns>
</page-vpn-ipsec-mobile>
<page-vpn-ipsec-listkeys>
<name>VPN: IPsec: Pre-Shared Keys List</name>
<patterns>
<pattern>vpn_ipsec_keys.php*</pattern>
</patterns>
</page-vpn-ipsec-listkeys>
<page-status-ipsec>
<name>Status: IPsec</name>
<patterns>
<pattern>diag_ipsec.php*</pattern>
</patterns>
</page-status-ipsec>
<page-status-ipsec-leases>
<name>Status: IPsec: Leasespage</name>
<patterns>
<pattern>diag_ipsec_leases.php*</pattern>
</patterns>
</page-status-ipsec-leases>
<page-status-ipsec-sad>
<name>Status: IPsec: SAD</name>
<patterns>
<pattern>diag_ipsec_sad.php*</pattern>
</patterns>
</page-status-ipsec-sad>
<page-status-ipsec-spd>
<name>Status: IPsec: SPD</name>
<patterns>
<pattern>diag_ipsec_spd.php*</pattern>
</patterns>
</page-status-ipsec-spd>
<page-status-systemlogs-ipsecvpn>
<name>Status: System logs: IPsec VPN</name>
<patterns>
<pattern>diag_logs_ipsec.php*</pattern>
</patterns>
</page-status-systemlogs-ipsecvpn>
</acl>

View File

@ -0,0 +1,191 @@
<?php
/*
* Copyright (C) 2019 Pascal Mathis <mail@pascalmathis.com>
* 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
];
}
}

View File

@ -0,0 +1,30 @@
<model>
<mount>//OPNsense/IPsec</mount>
<description>
OPNsense IPsec
</description>
<items>
<keyPairs>
<keyPair type="ArrayField">
<name type="TextField">
<Required>Y</Required>
</name>
<keyType type="OptionField">
<Required>Y</Required>
<default>rsa</default>
<OptionValues>
<rsa>RSA</rsa>
</OptionValues>
</keyType>
<publicKey type="TextField">
<Required>Y</Required>
</publicKey>
<privateKey type="TextField">
<Required>N</Required>
</privateKey>
<keySize type="IntegerField"/>
<keyFingerprint type="TextField"/>
</keyPair>
</keyPairs>
</items>
</model>

View File

@ -0,0 +1,25 @@
<menu>
<VPN>
<IPsec cssClass="fa fa-lock fa-fw" order="10">
<Tunnels order="10" VisibleName="Tunnel Settings" url="/vpn_ipsec.php">
<Phase1 url="/vpn_ipsec_phase1.php*" visibility="hidden"/>
<Phase2 url="/vpn_ipsec_phase2.php*" visibility="hidden"/>
</Tunnels>
<Mobile order="20" VisibleName="Mobile Clients" url="/vpn_ipsec_mobile.php">
<Act url="/vpn_ipsec_mobile.php*" visibility="hidden"/>
</Mobile>
<Keys order="30" VisibleName="Pre-Shared Keys" url="/vpn_ipsec_keys.php">
<Edit url="/vpn_ipsec_keys_edit.php*" visibility="hidden"/>
</Keys>
<KeyPairs order="40" VisibleName="RSA Key Pairs" url="/ui/ipsec/key-pairs" />
<Settings order="50" VisibleName="Advanced Settings" url="/vpn_ipsec_settings.php"/>
<Status order="60" VisibleName="Status Overview" url="/diag_ipsec.php">
<Act url="/diag_ipsec.php?*" visibility="hidden"/>
</Status>
<Leases order="70" VisibleName="Lease Status" url="/diag_ipsec_leases.php"/>
<SAD order="80" VisibleName="Security Association Database" url="/diag_ipsec_sad.php"/>
<SPD order="90" VisibleName="Security Policy Database" url="/diag_ipsec_spd.php"/>
<LogFile order="100" VisibleName="Log File" url="/diag_logs_ipsec.php"/>
</IPsec>
</VPN>
</menu>

View File

@ -0,0 +1,124 @@
{#
Copyright (C) 2019 Pascal Mathis <mail@pascalmathis.com>
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.
#}
<script>
$(function () {
const $applyLegacyConfig = $('#applyLegacyConfig');
const $applyLegacyConfigProgress = $('#applyLegacyConfigProgress');
const $responseMsg = $('#responseMsg');
const $dirtySubsystemMsg = $('#dirtySubsystemMsg');
// Helper method to fetch the current status of the legacy subsystem for viewing/hiding the "pending changes" alert
function updateLegacyStatus() {
ajaxCall('/api/ipsec/legacy-subsystem/status', {}, function (data, status) {
if (data['isDirty']) {
$responseMsg.addClass('hidden');
$dirtySubsystemMsg.removeClass('hidden');
} else {
$dirtySubsystemMsg.addClass('hidden');
}
});
}
// Apply config in legacy subsystem
$applyLegacyConfig.on('click', function (e) {
e.preventDefault();
$applyLegacyConfig.prop('disabled', true);
$applyLegacyConfigProgress.addClass('fa fa-spinner fa-pulse');
ajaxCall('/api/ipsec/legacy-subsystem/applyConfig', {}, function (data, status) {
// Preliminarily hide the "pending changes" alert and display the response message if available
if (data['message']) {
$dirtySubsystemMsg.addClass('hidden');
$responseMsg.removeClass('hidden').text(data['message']);
}
// Reset the state of the "apply changes" button
$applyLegacyConfig.prop('disabled', false);
$applyLegacyConfigProgress.removeClass('fa fa-spinner fa-pulse');
// Fetch the current legacy subsystem status to ensure changes have been processed
updateLegacyStatus();
});
});
// Initialize grid for displaying and manipulating key pairs
const $grid = $('#grid-key-pairs').UIBootgrid({
search: '/api/ipsec/key-pairs/searchItem',
get: '/api/ipsec/key-pairs/getItem/',
set: '/api/ipsec/key-pairs/setItem/',
add: '/api/ipsec/key-pairs/addItem/',
del: '/api/ipsec/key-pairs/delItem/',
});
// Refresh status of legacy subsystem when grid has changed
$grid.on('loaded.rs.jquery.bootgrid', updateLegacyStatus);
});
</script>
<div class="alert alert-info alert-dismissible hidden" role="alert" id="responseMsg"></div>
<div class="alert alert-info hidden" role="alert" id="dirtySubsystemMsg">
<button class="btn btn-primary pull-right" type="button" id="applyLegacyConfig">
<i id="applyLegacyConfigProgress" class=""></i>
{{ lang._('Apply changes') }}
</button>
<div>
{{ lang._('The IPsec tunnel configuration has been changed.') }}<br/>
{{ lang._('You must apply the changes in order for them to take effect.') }}
</div>
</div>
<div class="content-box">
<table id="grid-key-pairs" class="table table-condensed table-hover table-striped" data-editDialog="DialogKeyPair">
<thead>
<tr>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="name" data-type="string">{{ lang._('Name') }}</th>
<th data-column-id="keyType" data-width="20em" data-type="string">{{ lang._('Key Type') }}</th>
<th data-column-id="keySize" data-width="20em" data-type="number">{{ lang._('Key Size') }}</th>
<th data-column-id="keyFingerprint" data-type="string">{{ lang._('Key Fingerprint') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands"
data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-default">
<span class="fa fa-plus"></span>
</button>
<button data-action="deleteSelected" type="button" class="btn btn-xs btn-default">
<span class="fa fa-trash-o"></span>
</button>
</td>
</tr>
</tfoot>
</table>
</div>
{{ partial("layout_partials/base_dialog",['fields':formDialogKeyPair,'id':'DialogKeyPair','label':lang._('Edit key pair')]) }}

View File

@ -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

View File

@ -1,6 +1,7 @@
<?php
/*
* Copyright (C) 2019 Pascal Mathis <mail@pascalmathis.com>
* Copyright (C) 2014-2015 Deciso B.V.
* Copyright (C) 2008 Shrew Soft Inc. <mgrooms@shrew.net>
* Copyright (C) 2003-2005 Manuel Kasper <mk@neon1.net>
@ -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; ?>
</tr>
<?php
if (empty($pconfig['mobile'])):?>
<tr class="auth_opt auth_eap_tls auth_psk">
<tr class="auth_opt auth_eap_tls auth_psk auth_pubkey">
<td><i class="fa fa-info-circle text-muted"></i> <?=gettext("Peer identifier"); ?></td>
<td>
<select name="peerid_type" id="peerid_type">
@ -878,6 +895,52 @@ endforeach; ?>
</div>
</td>
</tr>
<tr class="auth_opt auth_pubkey">
<td><a id="help_for_pubkey_local" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Local Key Pair"); ?></td>
<td>
<select name="local-kpref">
<?php
foreach (ipsec_keypairs() as $keypair_uuid => $keypair) :
if ($keypair['publicKey'] and $keypair['privateKey']) :
?>
<option value="<?= $keypair_uuid; ?>" <?= isset($pconfig['local-kpref']) && $pconfig['local-kpref'] == $keypair_uuid ? "selected=\"selected\"" : "" ?>>
<?= $keypair['name']; ?>
</option>
<?php
endif;
endforeach;
?>
</select>
<div class="hidden" data-for="help_for_pubkey_local">
<?= gettext("Select a local key pair previously configured at IPsec \\ Key Pairs."); ?>
<br />
<?= gettext("This selection will only display key pairs which have both a public and private key."); ?>
</div>
</td>
</tr>
<tr class="auth_opt auth_pubkey">
<td><a id="help_for_pubkey_peer" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Peer Key Pair"); ?></td>
<td>
<select name="peer-kpref">
<?php
foreach (ipsec_keypairs() as $keypair_uuid => $keypair) :
if ($keypair['publicKey']) :
?>
<option value="<?= $keypair_uuid; ?>" <?= isset($pconfig['peer-kpref']) && $pconfig['peer-kpref'] == $keypair_uuid ? "selected=\"selected\"" : "" ?>>
<?= $keypair['name']; ?>
</option>
<?php
endif;
endforeach;
?>
</select>
<div class="hidden" data-for="help_for_pubkey_peer">
<?=gettext("Select a peer key pair previously configured at IPsec \\ Key Pairs."); ?>
<br />
<?= gettext("This selection will only display key pairs which have a public key."); ?>
</div>
</td>
</tr>
<tr class="auth_opt auth_eap_radius">
<td><a id="help_for_authservers" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Radius servers"); ?></td>
<td>