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 <mail@pascalmathis.com>
This commit is contained in:
Pascal Mathis 2019-08-25 21:44:26 +02:00
parent ae5692b477
commit 5d9183aa13
No known key found for this signature in database
GPG Key ID: E208DBA7BFC9B28C
12 changed files with 779 additions and 7 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

@ -0,0 +1,11 @@
<acl>
<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>
</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,7 @@
<menu>
<VPN>
<IPsec>
<PublicKeys order="31" VisibleName="RSA Key Pairs" url="/ui/ipsec/key-pairs" />
</IPsec>
</VPN>
</menu>

View File

@ -0,0 +1,123 @@
{#
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>
<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>
{{ 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>