mirror of
https://github.com/OpenVPN/openvpn.git
synced 2024-09-20 12:02:28 +02:00
31ea2ee4ca
Signed-off-by: Adriaan de Jong <dejong@fox-it.com> Acked-by: David Sommerseth <davids@redhat.com> Signed-off-by: David Sommerseth <davids@redhat.com>
1244 lines
33 KiB
C
1244 lines
33 KiB
C
/*
|
|
* OpenVPN -- An application to securely tunnel IP networks
|
|
* over a single TCP/UDP port, with support for SSL/TLS-based
|
|
* session authentication and key exchange,
|
|
* packet encryption, packet authentication, and
|
|
* packet compression.
|
|
*
|
|
* Copyright (C) 2002-2010 OpenVPN Technologies, Inc. <sales@openvpn.net>
|
|
* Copyright (C) 2010 Fox Crypto B.V. <openvpn@fox-it.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2
|
|
* as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program (see the file COPYING included with this
|
|
* distribution); if not, write to the Free Software Foundation, Inc.,
|
|
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
/**
|
|
* @file Control Channel Verification Module
|
|
*/
|
|
|
|
#include "syshead.h"
|
|
|
|
#if defined(USE_CRYPTO) && defined(USE_SSL)
|
|
|
|
#include "misc.h"
|
|
#include "manage.h"
|
|
#include "ssl_verify.h"
|
|
#include "ssl_verify_backend.h"
|
|
|
|
#ifdef USE_OPENSSL
|
|
#include "ssl_verify_openssl.h"
|
|
#endif
|
|
|
|
/** Legal characters in an X509 name */
|
|
#define X509_NAME_CHAR_CLASS (CC_ALNUM|CC_UNDERBAR|CC_DASH|CC_DOT|CC_AT|CC_COLON|CC_SLASH|CC_EQUAL)
|
|
|
|
/** Legal characters in a common name */
|
|
#define COMMON_NAME_CHAR_CLASS (CC_ALNUM|CC_UNDERBAR|CC_DASH|CC_DOT|CC_AT|CC_SLASH)
|
|
|
|
/** Maximum length of common name */
|
|
#define TLS_USERNAME_LEN 64
|
|
|
|
static void
|
|
string_mod_sslname (char *str, const unsigned int restrictive_flags, const unsigned int ssl_flags)
|
|
{
|
|
if (ssl_flags & SSLF_NO_NAME_REMAPPING)
|
|
string_mod (str, CC_PRINT, CC_CRLF, '_');
|
|
else
|
|
string_mod (str, restrictive_flags, 0, '_');
|
|
}
|
|
|
|
/*
|
|
* Export the untrusted IP address and port to the environment
|
|
*/
|
|
static void
|
|
setenv_untrusted (struct tls_session *session)
|
|
{
|
|
setenv_link_socket_actual (session->opt->es, "untrusted", &session->untrusted_addr, SA_IP_PORT);
|
|
}
|
|
|
|
/*
|
|
* Remove authenticated state from all sessions in the given tunnel
|
|
*/
|
|
static void
|
|
tls_deauthenticate (struct tls_multi *multi)
|
|
{
|
|
if (multi)
|
|
{
|
|
int i, j;
|
|
for (i = 0; i < TM_SIZE; ++i)
|
|
for (j = 0; j < KS_SIZE; ++j)
|
|
multi->session[i].key[j].authenticated = false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Set the given session's common_name
|
|
*/
|
|
static void
|
|
set_common_name (struct tls_session *session, const char *common_name)
|
|
{
|
|
if (session->common_name)
|
|
{
|
|
free (session->common_name);
|
|
session->common_name = NULL;
|
|
#ifdef ENABLE_PF
|
|
session->common_name_hashval = 0;
|
|
#endif
|
|
}
|
|
if (common_name)
|
|
{
|
|
session->common_name = string_alloc (common_name, NULL);
|
|
#ifdef ENABLE_PF
|
|
{
|
|
const uint32_t len = (uint32_t) strlen (common_name);
|
|
if (len)
|
|
session->common_name_hashval = hash_func ((const uint8_t*)common_name, len+1, 0);
|
|
else
|
|
session->common_name_hashval = 0;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Retrieve the common name for the given tunnel's active session. If the
|
|
* common name is NULL or empty, return NULL if null is true, or "UNDEF" if
|
|
* null is false.
|
|
*/
|
|
const char *
|
|
tls_common_name (const struct tls_multi *multi, const bool null)
|
|
{
|
|
const char *ret = NULL;
|
|
if (multi)
|
|
ret = multi->session[TM_ACTIVE].common_name;
|
|
if (ret && strlen (ret))
|
|
return ret;
|
|
else if (null)
|
|
return NULL;
|
|
else
|
|
return "UNDEF";
|
|
}
|
|
|
|
/*
|
|
* Lock the common name for the given tunnel.
|
|
*/
|
|
void
|
|
tls_lock_common_name (struct tls_multi *multi)
|
|
{
|
|
const char *cn = multi->session[TM_ACTIVE].common_name;
|
|
if (cn && !multi->locked_cn)
|
|
multi->locked_cn = string_alloc (cn, NULL);
|
|
}
|
|
|
|
/*
|
|
* Lock the username for the given tunnel
|
|
*/
|
|
static bool
|
|
tls_lock_username (struct tls_multi *multi, const char *username)
|
|
{
|
|
if (multi->locked_username)
|
|
{
|
|
if (!username || strcmp (username, multi->locked_username))
|
|
{
|
|
msg (D_TLS_ERRORS, "TLS Auth Error: username attempted to change from '%s' to '%s' -- tunnel disabled",
|
|
multi->locked_username,
|
|
np(username));
|
|
|
|
/* disable the tunnel */
|
|
tls_deauthenticate (multi);
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (username)
|
|
multi->locked_username = string_alloc (username, NULL);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const char *
|
|
tls_username (const struct tls_multi *multi, const bool null)
|
|
{
|
|
const char *ret = NULL;
|
|
if (multi)
|
|
ret = multi->locked_username;
|
|
if (ret && strlen (ret))
|
|
return ret;
|
|
else if (null)
|
|
return NULL;
|
|
else
|
|
return "UNDEF";
|
|
}
|
|
|
|
void
|
|
cert_hash_remember (struct tls_session *session, const int error_depth, const unsigned char *sha1_hash)
|
|
{
|
|
if (error_depth >= 0 && error_depth < MAX_CERT_DEPTH)
|
|
{
|
|
if (!session->cert_hash_set)
|
|
ALLOC_OBJ_CLEAR (session->cert_hash_set, struct cert_hash_set);
|
|
if (!session->cert_hash_set->ch[error_depth])
|
|
ALLOC_OBJ (session->cert_hash_set->ch[error_depth], struct cert_hash);
|
|
{
|
|
struct cert_hash *ch = session->cert_hash_set->ch[error_depth];
|
|
memcpy (ch->sha1_hash, sha1_hash, SHA_DIGEST_LENGTH);
|
|
}
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
static void
|
|
cert_hash_print (const struct cert_hash_set *chs, int msglevel)
|
|
{
|
|
struct gc_arena gc = gc_new ();
|
|
msg (msglevel, "CERT_HASH");
|
|
if (chs)
|
|
{
|
|
int i;
|
|
for (i = 0; i < MAX_CERT_DEPTH; ++i)
|
|
{
|
|
const struct cert_hash *ch = chs->ch[i];
|
|
if (ch)
|
|
msg (msglevel, "%d:%s", i, format_hex(ch->sha1_hash, SHA_DIGEST_LENGTH, 0, &gc));
|
|
}
|
|
}
|
|
gc_free (&gc);
|
|
}
|
|
#endif
|
|
|
|
void
|
|
cert_hash_free (struct cert_hash_set *chs)
|
|
{
|
|
if (chs)
|
|
{
|
|
int i;
|
|
for (i = 0; i < MAX_CERT_DEPTH; ++i)
|
|
free (chs->ch[i]);
|
|
free (chs);
|
|
}
|
|
}
|
|
|
|
static bool
|
|
cert_hash_compare (const struct cert_hash_set *chs1, const struct cert_hash_set *chs2)
|
|
{
|
|
if (chs1 && chs2)
|
|
{
|
|
int i;
|
|
for (i = 0; i < MAX_CERT_DEPTH; ++i)
|
|
{
|
|
const struct cert_hash *ch1 = chs1->ch[i];
|
|
const struct cert_hash *ch2 = chs2->ch[i];
|
|
|
|
if (!ch1 && !ch2)
|
|
continue;
|
|
else if (ch1 && ch2 && !memcmp (ch1->sha1_hash, ch2->sha1_hash, SHA_DIGEST_LENGTH))
|
|
continue;
|
|
else
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
else if (!chs1 && !chs2)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
static struct cert_hash_set *
|
|
cert_hash_copy (const struct cert_hash_set *chs)
|
|
{
|
|
struct cert_hash_set *dest = NULL;
|
|
if (chs)
|
|
{
|
|
int i;
|
|
ALLOC_OBJ_CLEAR (dest, struct cert_hash_set);
|
|
for (i = 0; i < MAX_CERT_DEPTH; ++i)
|
|
{
|
|
const struct cert_hash *ch = chs->ch[i];
|
|
if (ch)
|
|
{
|
|
ALLOC_OBJ (dest->ch[i], struct cert_hash);
|
|
memcpy (dest->ch[i]->sha1_hash, ch->sha1_hash, SHA_DIGEST_LENGTH);
|
|
}
|
|
}
|
|
}
|
|
return dest;
|
|
}
|
|
void
|
|
tls_lock_cert_hash_set (struct tls_multi *multi)
|
|
{
|
|
const struct cert_hash_set *chs = multi->session[TM_ACTIVE].cert_hash_set;
|
|
if (chs && !multi->locked_cert_hash_set)
|
|
multi->locked_cert_hash_set = cert_hash_copy (chs);
|
|
}
|
|
|
|
/*
|
|
* Returns the string associated with the given certificate type.
|
|
*/
|
|
static const char *
|
|
print_nsCertType (int type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case NS_CERT_CHECK_SERVER:
|
|
return "SERVER";
|
|
case NS_CERT_CHECK_CLIENT:
|
|
return "CLIENT";
|
|
default:
|
|
return "?";
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Verify the peer's certificate fields.
|
|
*
|
|
* @param opt the tls options to verify against
|
|
* @param peer_cert the peer's certificate
|
|
* @param subject the peer's extracted subject name
|
|
* @param subject the peer's extracted common name
|
|
*/
|
|
static result_t
|
|
verify_peer_cert(const struct tls_options *opt, x509_cert_t *peer_cert,
|
|
const char *subject, const char *common_name)
|
|
{
|
|
/* verify certificate nsCertType */
|
|
if (opt->ns_cert_type != NS_CERT_CHECK_NONE)
|
|
{
|
|
if (SUCCESS == x509_verify_ns_cert_type (peer_cert, opt->ns_cert_type))
|
|
{
|
|
msg (D_HANDSHAKE, "VERIFY OK: nsCertType=%s",
|
|
print_nsCertType (opt->ns_cert_type));
|
|
}
|
|
else
|
|
{
|
|
msg (D_HANDSHAKE, "VERIFY nsCertType ERROR: %s, require nsCertType=%s",
|
|
subject, print_nsCertType (opt->ns_cert_type));
|
|
return FAILURE; /* Reject connection */
|
|
}
|
|
}
|
|
|
|
#if OPENSSL_VERSION_NUMBER >= 0x00907000L || USE_POLARSSL
|
|
|
|
/* verify certificate ku */
|
|
if (opt->remote_cert_ku[0] != 0)
|
|
{
|
|
if (SUCCESS == x509_verify_cert_ku (peer_cert, opt->remote_cert_ku, MAX_PARMS))
|
|
{
|
|
msg (D_HANDSHAKE, "VERIFY KU OK");
|
|
}
|
|
else
|
|
{
|
|
msg (D_HANDSHAKE, "VERIFY KU ERROR");
|
|
return FAILURE; /* Reject connection */
|
|
}
|
|
}
|
|
|
|
/* verify certificate eku */
|
|
if (opt->remote_cert_eku != NULL)
|
|
{
|
|
if (SUCCESS == x509_verify_cert_eku (peer_cert, opt->remote_cert_eku))
|
|
{
|
|
msg (D_HANDSHAKE, "VERIFY EKU OK");
|
|
}
|
|
else
|
|
{
|
|
msg (D_HANDSHAKE, "VERIFY EKU ERROR");
|
|
return FAILURE; /* Reject connection */
|
|
}
|
|
}
|
|
|
|
#endif /* OPENSSL_VERSION_NUMBER */
|
|
|
|
/* verify X509 name or common name against --tls-remote */
|
|
if (opt->verify_x509name && strlen (opt->verify_x509name) > 0)
|
|
{
|
|
if (strcmp (opt->verify_x509name, subject) == 0
|
|
|| strncmp (opt->verify_x509name, common_name, strlen (opt->verify_x509name)) == 0)
|
|
msg (D_HANDSHAKE, "VERIFY X509NAME OK: %s", subject);
|
|
else
|
|
{
|
|
msg (D_HANDSHAKE, "VERIFY X509NAME ERROR: %s, must be %s",
|
|
subject, opt->verify_x509name);
|
|
return FAILURE; /* Reject connection */
|
|
}
|
|
}
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
/*
|
|
* Export the subject, common_name, and raw certificate fields to the
|
|
* environment for later verification by scripts and plugins.
|
|
*/
|
|
static void
|
|
verify_cert_set_env(struct env_set *es, x509_cert_t *peer_cert, int cert_depth,
|
|
const char *subject, const char *common_name
|
|
#ifdef ENABLE_X509_TRACK
|
|
, const struct x509_track *x509_track
|
|
#endif
|
|
)
|
|
{
|
|
char envname[64];
|
|
|
|
/* Save X509 fields in environment */
|
|
#ifdef ENABLE_X509_TRACK
|
|
if (x509_track)
|
|
x509_setenv_track (x509_track, es, cert_depth, peer_cert);
|
|
else
|
|
#endif
|
|
x509_setenv (es, cert_depth, peer_cert);
|
|
|
|
/* export subject name string as environmental variable */
|
|
openvpn_snprintf (envname, sizeof(envname), "tls_id_%d", cert_depth);
|
|
setenv_str (es, envname, subject);
|
|
|
|
#if 0
|
|
/* export common name string as environmental variable */
|
|
openvpn_snprintf (envname, sizeof(envname), "tls_common_name_%d", cert_depth);
|
|
setenv_str (es, envname, common_name);
|
|
#endif
|
|
|
|
#ifdef ENABLE_EUREPHIA
|
|
/* export X509 cert SHA1 fingerprint */
|
|
{
|
|
struct gc_arena gc = gc_new ();
|
|
unsigned char *sha1_hash = x509_get_sha1_hash(peer_cert);
|
|
|
|
openvpn_snprintf (envname, sizeof(envname), "tls_digest_%d", cert_depth);
|
|
setenv_str (es, envname, format_hex_ex(sha1_hash, SHA_DIGEST_LENGTH, 0, 1,
|
|
":", &gc));
|
|
x509_free_sha1_hash(sha1_hash);
|
|
gc_free(&gc);
|
|
}
|
|
#endif
|
|
|
|
/* export serial number as environmental variable,
|
|
use bignum in case serial number is large */
|
|
{
|
|
char *serial = x509_get_serial(peer_cert);
|
|
openvpn_snprintf (envname, sizeof(envname), "tls_serial_%d", cert_depth);
|
|
setenv_str (es, envname, serial);
|
|
x509_free_serial(serial);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* call --tls-verify plug-in(s)
|
|
*/
|
|
static result_t
|
|
verify_cert_call_plugin(const struct plugin_list *plugins, struct env_set *es,
|
|
int cert_depth, x509_cert_t *cert, char *subject)
|
|
{
|
|
if (plugin_defined (plugins, OPENVPN_PLUGIN_TLS_VERIFY))
|
|
{
|
|
int ret;
|
|
struct argv argv = argv_new ();
|
|
|
|
argv_printf (&argv, "%d %s", cert_depth, subject);
|
|
|
|
ret = plugin_call_ssl (plugins, OPENVPN_PLUGIN_TLS_VERIFY, &argv, NULL, es, cert_depth, cert);
|
|
|
|
argv_reset (&argv);
|
|
|
|
if (ret == OPENVPN_PLUGIN_FUNC_SUCCESS)
|
|
{
|
|
msg (D_HANDSHAKE, "VERIFY PLUGIN OK: depth=%d, %s",
|
|
cert_depth, subject);
|
|
}
|
|
else
|
|
{
|
|
msg (D_HANDSHAKE, "VERIFY PLUGIN ERROR: depth=%d, %s",
|
|
cert_depth, subject);
|
|
return FAILURE; /* Reject connection */
|
|
}
|
|
}
|
|
return SUCCESS;
|
|
}
|
|
|
|
static const char *
|
|
verify_cert_export_cert(x509_cert_t *peercert, const char *tmp_dir, struct gc_arena *gc)
|
|
{
|
|
FILE *peercert_file;
|
|
const char *peercert_filename="";
|
|
|
|
if(!tmp_dir)
|
|
return NULL;
|
|
|
|
/* create tmp file to store peer cert */
|
|
peercert_filename = create_temp_file (tmp_dir, "pcf", gc);
|
|
|
|
/* write peer-cert in tmp-file */
|
|
peercert_file = fopen(peercert_filename, "w+");
|
|
if(!peercert_file)
|
|
{
|
|
msg (M_ERR, "Failed to open temporary file : %s", peercert_filename);
|
|
return NULL;
|
|
}
|
|
|
|
if (SUCCESS != x509_write_pem(peercert_file, peercert))
|
|
msg (M_ERR, "Error writing PEM file containing certificate");
|
|
|
|
fclose(peercert_file);
|
|
return peercert_filename;
|
|
}
|
|
|
|
|
|
/*
|
|
* run --tls-verify script
|
|
*/
|
|
static result_t
|
|
verify_cert_call_command(const char *verify_command, struct env_set *es,
|
|
int cert_depth, x509_cert_t *cert, char *subject, const char *verify_export_cert)
|
|
{
|
|
const char *tmp_file = NULL;
|
|
int ret;
|
|
struct gc_arena gc = gc_new();
|
|
struct argv argv = argv_new ();
|
|
|
|
setenv_str (es, "script_type", "tls-verify");
|
|
|
|
if (verify_export_cert)
|
|
{
|
|
if ((tmp_file=verify_cert_export_cert(cert, verify_export_cert, &gc)))
|
|
{
|
|
setenv_str(es, "peer_cert", tmp_file);
|
|
}
|
|
}
|
|
|
|
argv_printf (&argv, "%sc %d %s", verify_command, cert_depth, subject);
|
|
|
|
argv_msg_prefix (D_TLS_DEBUG, &argv, "TLS: executing verify command");
|
|
ret = openvpn_run_script (&argv, es, 0, "--tls-verify script");
|
|
|
|
if (verify_export_cert)
|
|
{
|
|
if (tmp_file)
|
|
delete_file(tmp_file);
|
|
}
|
|
|
|
gc_free(&gc);
|
|
argv_reset (&argv);
|
|
|
|
if (ret)
|
|
{
|
|
msg (D_HANDSHAKE, "VERIFY SCRIPT OK: depth=%d, %s",
|
|
cert_depth, subject);
|
|
return SUCCESS;
|
|
}
|
|
|
|
msg (D_HANDSHAKE, "VERIFY SCRIPT ERROR: depth=%d, %s",
|
|
cert_depth, subject);
|
|
return FAILURE; /* Reject connection */
|
|
}
|
|
|
|
/*
|
|
* check peer cert against CRL directory
|
|
*/
|
|
static result_t
|
|
verify_check_crl_dir(const char *crl_dir, x509_cert_t *cert)
|
|
{
|
|
char fn[256];
|
|
int fd;
|
|
char *serial = x509_get_serial(cert);
|
|
|
|
if (!openvpn_snprintf(fn, sizeof(fn), "%s%c%s", crl_dir, OS_SPECIFIC_DIRSEP, serial))
|
|
{
|
|
msg (D_HANDSHAKE, "VERIFY CRL: filename overflow");
|
|
x509_free_serial(serial);
|
|
return FAILURE;
|
|
}
|
|
fd = open (fn, O_RDONLY);
|
|
if (fd >= 0)
|
|
{
|
|
msg (D_HANDSHAKE, "VERIFY CRL: certificate serial number %s is revoked", serial);
|
|
x509_free_serial(serial);
|
|
close(fd);
|
|
return FAILURE;
|
|
}
|
|
|
|
x509_free_serial(serial);
|
|
|
|
return SUCCESS;
|
|
}
|
|
|
|
result_t
|
|
verify_cert(struct tls_session *session, x509_cert_t *cert, int cert_depth)
|
|
{
|
|
char *subject = NULL;
|
|
char common_name[TLS_USERNAME_LEN] = {0};
|
|
const struct tls_options *opt;
|
|
|
|
opt = session->opt;
|
|
ASSERT (opt);
|
|
|
|
session->verified = false;
|
|
|
|
/* get the X509 name */
|
|
subject = x509_get_subject(cert);
|
|
if (!subject)
|
|
{
|
|
msg (D_TLS_ERRORS, "VERIFY ERROR: depth=%d, could not extract X509 "
|
|
"subject string from certificate", cert_depth);
|
|
goto err;
|
|
}
|
|
|
|
/* enforce character class restrictions in X509 name */
|
|
string_mod_sslname (subject, X509_NAME_CHAR_CLASS, opt->ssl_flags);
|
|
string_replace_leading (subject, '-', '_');
|
|
|
|
/* extract the username (default is CN) */
|
|
if (SUCCESS != x509_get_username (common_name, TLS_USERNAME_LEN,
|
|
opt->x509_username_field, cert))
|
|
{
|
|
if (!cert_depth)
|
|
{
|
|
msg (D_TLS_ERRORS, "VERIFY ERROR: could not extract %s from X509 "
|
|
"subject string ('%s') -- note that the username length is "
|
|
"limited to %d characters",
|
|
opt->x509_username_field,
|
|
subject,
|
|
TLS_USERNAME_LEN);
|
|
goto err;
|
|
}
|
|
}
|
|
|
|
/* enforce character class restrictions in common name */
|
|
string_mod_sslname (common_name, COMMON_NAME_CHAR_CLASS, opt->ssl_flags);
|
|
|
|
/* warn if cert chain is too deep */
|
|
if (cert_depth >= MAX_CERT_DEPTH)
|
|
{
|
|
msg (D_TLS_ERRORS, "TLS Error: Convoluted certificate chain detected with depth [%d] greater than %d", cert_depth, MAX_CERT_DEPTH);
|
|
goto err; /* Reject connection */
|
|
}
|
|
|
|
/* verify level 1 cert, i.e. the CA that signed our leaf cert */
|
|
if (cert_depth == 1 && opt->verify_hash)
|
|
{
|
|
unsigned char *sha1_hash = x509_get_sha1_hash(cert);
|
|
if (memcmp (sha1_hash, opt->verify_hash, SHA_DIGEST_LENGTH))
|
|
{
|
|
msg (D_TLS_ERRORS, "TLS Error: level-1 certificate hash verification failed");
|
|
x509_free_sha1_hash(sha1_hash);
|
|
goto err;
|
|
}
|
|
x509_free_sha1_hash(sha1_hash);
|
|
}
|
|
|
|
/* save common name in session object */
|
|
if (cert_depth == 0)
|
|
set_common_name (session, common_name);
|
|
|
|
session->verify_maxlevel = max_int (session->verify_maxlevel, cert_depth);
|
|
|
|
/* export certificate values to the environment */
|
|
verify_cert_set_env(opt->es, cert, cert_depth, subject, common_name
|
|
#ifdef ENABLE_X509_TRACK
|
|
, opt->x509_track
|
|
#endif
|
|
);
|
|
|
|
/* export current untrusted IP */
|
|
setenv_untrusted (session);
|
|
|
|
/* If this is the peer's own certificate, verify it */
|
|
if (cert_depth == 0 && SUCCESS != verify_peer_cert(opt, cert, subject, common_name))
|
|
goto err;
|
|
|
|
/* call --tls-verify plug-in(s), if registered */
|
|
if (SUCCESS != verify_cert_call_plugin(opt->plugins, opt->es, cert_depth, cert, subject))
|
|
goto err;
|
|
|
|
/* run --tls-verify script */
|
|
if (opt->verify_command && SUCCESS != verify_cert_call_command(opt->verify_command,
|
|
opt->es, cert_depth, cert, subject, opt->verify_export_cert))
|
|
goto err;
|
|
|
|
/* check peer cert against CRL */
|
|
if (opt->crl_file)
|
|
{
|
|
if (opt->ssl_flags & SSLF_CRL_VERIFY_DIR)
|
|
{
|
|
if (SUCCESS != verify_check_crl_dir(opt->crl_file, cert))
|
|
goto err;
|
|
}
|
|
else
|
|
{
|
|
if (SUCCESS != x509_verify_crl(opt->crl_file, cert, subject))
|
|
goto err;
|
|
}
|
|
}
|
|
|
|
msg (D_HANDSHAKE, "VERIFY OK: depth=%d, %s", cert_depth, subject);
|
|
session->verified = true;
|
|
|
|
done:
|
|
x509_free_subject (subject);
|
|
return (session->verified == true) ? SUCCESS : FAILURE;
|
|
|
|
err:
|
|
tls_clear_error();
|
|
session->verified = false;
|
|
goto done;
|
|
}
|
|
|
|
/* ***************************************************************************
|
|
* Functions for the management of deferred authentication when using
|
|
* user/password authentication.
|
|
*************************************************************************** */
|
|
|
|
#ifdef ENABLE_DEF_AUTH
|
|
/* key_state_test_auth_control_file return values,
|
|
NOTE: acf_merge indexing depends on these values */
|
|
#define ACF_UNDEFINED 0
|
|
#define ACF_SUCCEEDED 1
|
|
#define ACF_DISABLED 2
|
|
#define ACF_FAILED 3
|
|
#endif
|
|
|
|
#ifdef MANAGEMENT_DEF_AUTH
|
|
void
|
|
man_def_auth_set_client_reason (struct tls_multi *multi, const char *client_reason)
|
|
{
|
|
if (multi->client_reason)
|
|
{
|
|
free (multi->client_reason);
|
|
multi->client_reason = NULL;
|
|
}
|
|
if (client_reason && strlen (client_reason))
|
|
multi->client_reason = string_alloc (client_reason, NULL);
|
|
}
|
|
|
|
static inline unsigned int
|
|
man_def_auth_test (const struct key_state *ks)
|
|
{
|
|
if (management_enable_def_auth (management))
|
|
return ks->mda_status;
|
|
else
|
|
return ACF_DISABLED;
|
|
}
|
|
#endif
|
|
|
|
#ifdef PLUGIN_DEF_AUTH
|
|
|
|
/*
|
|
* auth_control_file functions
|
|
*/
|
|
|
|
void
|
|
key_state_rm_auth_control_file (struct key_state *ks)
|
|
{
|
|
if (ks && ks->auth_control_file)
|
|
{
|
|
delete_file (ks->auth_control_file);
|
|
free (ks->auth_control_file);
|
|
ks->auth_control_file = NULL;
|
|
}
|
|
}
|
|
|
|
static void
|
|
key_state_gen_auth_control_file (struct key_state *ks, const struct tls_options *opt)
|
|
{
|
|
struct gc_arena gc = gc_new ();
|
|
const char *acf;
|
|
|
|
key_state_rm_auth_control_file (ks);
|
|
acf = create_temp_file (opt->tmp_dir, "acf", &gc);
|
|
if (acf) {
|
|
ks->auth_control_file = string_alloc (acf, NULL);
|
|
setenv_str (opt->es, "auth_control_file", ks->auth_control_file);
|
|
} /* FIXME: Should have better error handling? */
|
|
|
|
gc_free (&gc);
|
|
}
|
|
|
|
static unsigned int
|
|
key_state_test_auth_control_file (struct key_state *ks)
|
|
{
|
|
if (ks && ks->auth_control_file)
|
|
{
|
|
unsigned int ret = ks->auth_control_status;
|
|
if (ret == ACF_UNDEFINED)
|
|
{
|
|
FILE *fp = fopen (ks->auth_control_file, "r");
|
|
if (fp)
|
|
{
|
|
const int c = fgetc (fp);
|
|
if (c == '1')
|
|
ret = ACF_SUCCEEDED;
|
|
else if (c == '0')
|
|
ret = ACF_FAILED;
|
|
fclose (fp);
|
|
ks->auth_control_status = ret;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
return ACF_DISABLED;
|
|
}
|
|
|
|
#endif
|
|
|
|
/*
|
|
* Return current session authentication state. Return
|
|
* value is TLS_AUTHENTICATION_x.
|
|
*/
|
|
|
|
int
|
|
tls_authentication_status (struct tls_multi *multi, const int latency)
|
|
{
|
|
bool deferred = false;
|
|
bool success = false;
|
|
bool active = false;
|
|
|
|
#ifdef ENABLE_DEF_AUTH
|
|
static const unsigned char acf_merge[] =
|
|
{
|
|
ACF_UNDEFINED, /* s1=ACF_UNDEFINED s2=ACF_UNDEFINED */
|
|
ACF_UNDEFINED, /* s1=ACF_UNDEFINED s2=ACF_SUCCEEDED */
|
|
ACF_UNDEFINED, /* s1=ACF_UNDEFINED s2=ACF_DISABLED */
|
|
ACF_FAILED, /* s1=ACF_UNDEFINED s2=ACF_FAILED */
|
|
ACF_UNDEFINED, /* s1=ACF_SUCCEEDED s2=ACF_UNDEFINED */
|
|
ACF_SUCCEEDED, /* s1=ACF_SUCCEEDED s2=ACF_SUCCEEDED */
|
|
ACF_SUCCEEDED, /* s1=ACF_SUCCEEDED s2=ACF_DISABLED */
|
|
ACF_FAILED, /* s1=ACF_SUCCEEDED s2=ACF_FAILED */
|
|
ACF_UNDEFINED, /* s1=ACF_DISABLED s2=ACF_UNDEFINED */
|
|
ACF_SUCCEEDED, /* s1=ACF_DISABLED s2=ACF_SUCCEEDED */
|
|
ACF_DISABLED, /* s1=ACF_DISABLED s2=ACF_DISABLED */
|
|
ACF_FAILED, /* s1=ACF_DISABLED s2=ACF_FAILED */
|
|
ACF_FAILED, /* s1=ACF_FAILED s2=ACF_UNDEFINED */
|
|
ACF_FAILED, /* s1=ACF_FAILED s2=ACF_SUCCEEDED */
|
|
ACF_FAILED, /* s1=ACF_FAILED s2=ACF_DISABLED */
|
|
ACF_FAILED /* s1=ACF_FAILED s2=ACF_FAILED */
|
|
};
|
|
#endif /* ENABLE_DEF_AUTH */
|
|
|
|
if (multi)
|
|
{
|
|
int i;
|
|
|
|
#ifdef ENABLE_DEF_AUTH
|
|
if (latency && multi->tas_last && multi->tas_last + latency >= now)
|
|
return TLS_AUTHENTICATION_UNDEFINED;
|
|
multi->tas_last = now;
|
|
#endif /* ENABLE_DEF_AUTH */
|
|
|
|
for (i = 0; i < KEY_SCAN_SIZE; ++i)
|
|
{
|
|
struct key_state *ks = multi->key_scan[i];
|
|
if (DECRYPT_KEY_ENABLED (multi, ks))
|
|
{
|
|
active = true;
|
|
if (ks->authenticated)
|
|
{
|
|
#ifdef ENABLE_DEF_AUTH
|
|
unsigned int s1 = ACF_DISABLED;
|
|
unsigned int s2 = ACF_DISABLED;
|
|
#ifdef PLUGIN_DEF_AUTH
|
|
s1 = key_state_test_auth_control_file (ks);
|
|
#endif /* PLUGIN_DEF_AUTH */
|
|
#ifdef MANAGEMENT_DEF_AUTH
|
|
s2 = man_def_auth_test (ks);
|
|
#endif /* MANAGEMENT_DEF_AUTH */
|
|
ASSERT (s1 < 4 && s2 < 4);
|
|
switch (acf_merge[(s1<<2) + s2])
|
|
{
|
|
case ACF_SUCCEEDED:
|
|
case ACF_DISABLED:
|
|
success = true;
|
|
ks->auth_deferred = false;
|
|
break;
|
|
case ACF_UNDEFINED:
|
|
if (now < ks->auth_deferred_expire)
|
|
deferred = true;
|
|
break;
|
|
case ACF_FAILED:
|
|
ks->authenticated = false;
|
|
break;
|
|
default:
|
|
ASSERT (0);
|
|
}
|
|
#else /* !ENABLE_DEF_AUTH */
|
|
success = true;
|
|
#endif /* ENABLE_DEF_AUTH */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
dmsg (D_TLS_ERRORS, "TAS: a=%d s=%d d=%d", active, success, deferred);
|
|
#endif
|
|
|
|
if (success)
|
|
return TLS_AUTHENTICATION_SUCCEEDED;
|
|
else if (!active || deferred)
|
|
return TLS_AUTHENTICATION_DEFERRED;
|
|
else
|
|
return TLS_AUTHENTICATION_FAILED;
|
|
}
|
|
|
|
#ifdef MANAGEMENT_DEF_AUTH
|
|
/*
|
|
* For deferred auth, this is where the management interface calls (on server)
|
|
* to indicate auth failure/success.
|
|
*/
|
|
bool
|
|
tls_authenticate_key (struct tls_multi *multi, const unsigned int mda_key_id, const bool auth, const char *client_reason)
|
|
{
|
|
bool ret = false;
|
|
if (multi)
|
|
{
|
|
int i;
|
|
man_def_auth_set_client_reason (multi, client_reason);
|
|
for (i = 0; i < KEY_SCAN_SIZE; ++i)
|
|
{
|
|
struct key_state *ks = multi->key_scan[i];
|
|
if (ks->mda_key_id == mda_key_id)
|
|
{
|
|
ks->mda_status = auth ? ACF_SUCCEEDED : ACF_FAILED;
|
|
ret = true;
|
|
}
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
#endif
|
|
|
|
|
|
/* ****************************************************************************
|
|
* Functions to verify username and password
|
|
*
|
|
* Authenticate a client using username/password.
|
|
* Runs on server.
|
|
*
|
|
* If you want to add new authentication methods,
|
|
* this is the place to start.
|
|
*************************************************************************** */
|
|
|
|
/*
|
|
* Verify the user name and password using a script
|
|
*/
|
|
static bool
|
|
verify_user_pass_script (struct tls_session *session, const struct user_pass *up)
|
|
{
|
|
struct gc_arena gc = gc_new ();
|
|
struct argv argv = argv_new ();
|
|
const char *tmp_file = "";
|
|
bool ret = false;
|
|
|
|
/* Is username defined? */
|
|
if ((session->opt->ssl_flags & SSLF_AUTH_USER_PASS_OPTIONAL) || strlen (up->username))
|
|
{
|
|
/* Set environmental variables prior to calling script */
|
|
setenv_str (session->opt->es, "script_type", "user-pass-verify");
|
|
|
|
if (session->opt->auth_user_pass_verify_script_via_file)
|
|
{
|
|
struct status_output *so;
|
|
|
|
tmp_file = create_temp_file (session->opt->tmp_dir, "up", &gc);
|
|
if( tmp_file ) {
|
|
so = status_open (tmp_file, 0, -1, NULL, STATUS_OUTPUT_WRITE);
|
|
status_printf (so, "%s", up->username);
|
|
status_printf (so, "%s", up->password);
|
|
if (!status_close (so))
|
|
{
|
|
msg (D_TLS_ERRORS, "TLS Auth Error: could not write username/password to file: %s",
|
|
tmp_file);
|
|
goto done;
|
|
}
|
|
} else {
|
|
msg (D_TLS_ERRORS, "TLS Auth Error: could not create write "
|
|
"username/password to temp file");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
setenv_str (session->opt->es, "username", up->username);
|
|
setenv_str (session->opt->es, "password", up->password);
|
|
}
|
|
|
|
/* setenv incoming cert common name for script */
|
|
setenv_str (session->opt->es, "common_name", session->common_name);
|
|
|
|
/* setenv client real IP address */
|
|
setenv_untrusted (session);
|
|
|
|
/* format command line */
|
|
argv_printf (&argv, "%sc %s", session->opt->auth_user_pass_verify_script, tmp_file);
|
|
|
|
/* call command */
|
|
ret = openvpn_run_script (&argv, session->opt->es, 0,
|
|
"--auth-user-pass-verify");
|
|
|
|
if (!session->opt->auth_user_pass_verify_script_via_file)
|
|
setenv_del (session->opt->es, "password");
|
|
}
|
|
else
|
|
{
|
|
msg (D_TLS_ERRORS, "TLS Auth Error: peer provided a blank username");
|
|
}
|
|
|
|
done:
|
|
if (tmp_file && strlen (tmp_file) > 0)
|
|
delete_file (tmp_file);
|
|
|
|
argv_reset (&argv);
|
|
gc_free (&gc);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Verify the username and password using a plugin
|
|
*/
|
|
static int
|
|
verify_user_pass_plugin (struct tls_session *session, const struct user_pass *up, const char *raw_username)
|
|
{
|
|
int retval = OPENVPN_PLUGIN_FUNC_ERROR;
|
|
struct key_state *ks = &session->key[KS_PRIMARY]; /* primary key */
|
|
|
|
/* Is username defined? */
|
|
if ((session->opt->ssl_flags & SSLF_AUTH_USER_PASS_OPTIONAL) || strlen (up->username))
|
|
{
|
|
/* set username/password in private env space */
|
|
setenv_str (session->opt->es, "username", raw_username);
|
|
setenv_str (session->opt->es, "password", up->password);
|
|
|
|
/* setenv incoming cert common name for script */
|
|
setenv_str (session->opt->es, "common_name", session->common_name);
|
|
|
|
/* setenv client real IP address */
|
|
setenv_untrusted (session);
|
|
|
|
#ifdef PLUGIN_DEF_AUTH
|
|
/* generate filename for deferred auth control file */
|
|
key_state_gen_auth_control_file (ks, session->opt);
|
|
#endif
|
|
|
|
/* call command */
|
|
retval = plugin_call (session->opt->plugins, OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY, NULL, NULL, session->opt->es);
|
|
|
|
#ifdef PLUGIN_DEF_AUTH
|
|
/* purge auth control filename (and file itself) for non-deferred returns */
|
|
if (retval != OPENVPN_PLUGIN_FUNC_DEFERRED)
|
|
key_state_rm_auth_control_file (ks);
|
|
#endif
|
|
|
|
setenv_del (session->opt->es, "password");
|
|
setenv_str (session->opt->es, "username", up->username);
|
|
}
|
|
else
|
|
{
|
|
msg (D_TLS_ERRORS, "TLS Auth Error (verify_user_pass_plugin): peer provided a blank username");
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
|
|
#ifdef MANAGEMENT_DEF_AUTH
|
|
/*
|
|
* MANAGEMENT_DEF_AUTH internal ssl_verify.c status codes
|
|
*/
|
|
#define KMDA_ERROR 0
|
|
#define KMDA_SUCCESS 1
|
|
#define KMDA_UNDEF 2
|
|
#define KMDA_DEF 3
|
|
|
|
static int
|
|
verify_user_pass_management (struct tls_session *session, const struct user_pass *up, const char *raw_username)
|
|
{
|
|
int retval = KMDA_ERROR;
|
|
struct key_state *ks = &session->key[KS_PRIMARY]; /* primary key */
|
|
|
|
/* Is username defined? */
|
|
if ((session->opt->ssl_flags & SSLF_AUTH_USER_PASS_OPTIONAL) || strlen (up->username))
|
|
{
|
|
/* set username/password in private env space */
|
|
setenv_str (session->opt->es, "username", raw_username);
|
|
setenv_str (session->opt->es, "password", up->password);
|
|
|
|
/* setenv incoming cert common name for script */
|
|
setenv_str (session->opt->es, "common_name", session->common_name);
|
|
|
|
/* setenv client real IP address */
|
|
setenv_untrusted (session);
|
|
|
|
if (management)
|
|
management_notify_client_needing_auth (management, ks->mda_key_id, session->opt->mda_context, session->opt->es);
|
|
|
|
setenv_del (session->opt->es, "password");
|
|
setenv_str (session->opt->es, "username", up->username);
|
|
|
|
retval = KMDA_SUCCESS;
|
|
}
|
|
else
|
|
{
|
|
msg (D_TLS_ERRORS, "TLS Auth Error (verify_user_pass_management): peer provided a blank username");
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
* Main username/password verification entry point
|
|
*/
|
|
void
|
|
verify_user_pass(struct user_pass *up, struct tls_multi *multi,
|
|
struct tls_session *session)
|
|
{
|
|
int s1 = OPENVPN_PLUGIN_FUNC_SUCCESS;
|
|
bool s2 = true;
|
|
struct key_state *ks = &session->key[KS_PRIMARY]; /* primary key */
|
|
|
|
struct gc_arena gc = gc_new ();
|
|
char *raw_username;
|
|
|
|
#ifdef MANAGEMENT_DEF_AUTH
|
|
int man_def_auth = KMDA_UNDEF;
|
|
|
|
if (management_enable_def_auth (management))
|
|
man_def_auth = KMDA_DEF;
|
|
#endif
|
|
|
|
/* preserve raw username before string_mod remapping, for plugins */
|
|
ALLOC_ARRAY_CLEAR_GC (raw_username, char, USER_PASS_LEN, &gc);
|
|
strcpy (raw_username, up->username);
|
|
string_mod (raw_username, CC_PRINT, CC_CRLF, '_');
|
|
|
|
/* enforce character class restrictions in username/password */
|
|
string_mod_sslname (up->username, COMMON_NAME_CHAR_CLASS, session->opt->ssl_flags);
|
|
string_mod (up->password, CC_PRINT, CC_CRLF, '_');
|
|
|
|
/* call plugin(s) and/or script */
|
|
#ifdef MANAGEMENT_DEF_AUTH
|
|
if (man_def_auth == KMDA_DEF)
|
|
man_def_auth = verify_user_pass_management (session, up, raw_username);
|
|
#endif
|
|
if (plugin_defined (session->opt->plugins, OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY))
|
|
s1 = verify_user_pass_plugin (session, up, raw_username);
|
|
if (session->opt->auth_user_pass_verify_script)
|
|
s2 = verify_user_pass_script (session, up);
|
|
|
|
/* check sizing of username if it will become our common name */
|
|
if ((session->opt->ssl_flags & SSLF_USERNAME_AS_COMMON_NAME) && strlen (up->username) >= TLS_USERNAME_LEN)
|
|
{
|
|
msg (D_TLS_ERRORS, "TLS Auth Error: --username-as-common name specified and username is longer than the maximum permitted Common Name length of %d characters", TLS_USERNAME_LEN);
|
|
s1 = OPENVPN_PLUGIN_FUNC_ERROR;
|
|
}
|
|
|
|
/* auth succeeded? */
|
|
if ((s1 == OPENVPN_PLUGIN_FUNC_SUCCESS
|
|
#ifdef PLUGIN_DEF_AUTH
|
|
|| s1 == OPENVPN_PLUGIN_FUNC_DEFERRED
|
|
#endif
|
|
) && s2
|
|
#ifdef MANAGEMENT_DEF_AUTH
|
|
&& man_def_auth != KMDA_ERROR
|
|
#endif
|
|
&& tls_lock_username (multi, up->username))
|
|
{
|
|
ks->authenticated = true;
|
|
#ifdef PLUGIN_DEF_AUTH
|
|
if (s1 == OPENVPN_PLUGIN_FUNC_DEFERRED)
|
|
ks->auth_deferred = true;
|
|
#endif
|
|
#ifdef MANAGEMENT_DEF_AUTH
|
|
if (man_def_auth != KMDA_UNDEF)
|
|
ks->auth_deferred = true;
|
|
#endif
|
|
if ((session->opt->ssl_flags & SSLF_USERNAME_AS_COMMON_NAME))
|
|
set_common_name (session, up->username);
|
|
#ifdef ENABLE_DEF_AUTH
|
|
msg (D_HANDSHAKE, "TLS: Username/Password authentication %s for username '%s' %s",
|
|
ks->auth_deferred ? "deferred" : "succeeded",
|
|
up->username,
|
|
(session->opt->ssl_flags & SSLF_USERNAME_AS_COMMON_NAME) ? "[CN SET]" : "");
|
|
#else
|
|
msg (D_HANDSHAKE, "TLS: Username/Password authentication %s for username '%s' %s",
|
|
"succeeded",
|
|
up->username,
|
|
(session->opt->ssl_flags & SSLF_USERNAME_AS_COMMON_NAME) ? "[CN SET]" : "");
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
msg (D_TLS_ERRORS, "TLS Auth Error: Auth Username/Password verification failed for peer");
|
|
}
|
|
|
|
gc_free (&gc);
|
|
}
|
|
|
|
void
|
|
verify_final_auth_checks(struct tls_multi *multi, struct tls_session *session)
|
|
{
|
|
struct key_state *ks = &session->key[KS_PRIMARY]; /* primary key */
|
|
|
|
/* While it shouldn't really happen, don't allow the common name to be NULL */
|
|
if (!session->common_name)
|
|
set_common_name (session, "");
|
|
|
|
/* Don't allow the CN to change once it's been locked */
|
|
if (ks->authenticated && multi->locked_cn)
|
|
{
|
|
const char *cn = session->common_name;
|
|
if (cn && strcmp (cn, multi->locked_cn))
|
|
{
|
|
msg (D_TLS_ERRORS, "TLS Auth Error: TLS object CN attempted to change from '%s' to '%s' -- tunnel disabled",
|
|
multi->locked_cn,
|
|
cn);
|
|
|
|
/* change the common name back to its original value and disable the tunnel */
|
|
set_common_name (session, multi->locked_cn);
|
|
tls_deauthenticate (multi);
|
|
}
|
|
}
|
|
|
|
/* Don't allow the cert hashes to change once they have been locked */
|
|
if (ks->authenticated && multi->locked_cert_hash_set)
|
|
{
|
|
const struct cert_hash_set *chs = session->cert_hash_set;
|
|
if (chs && !cert_hash_compare (chs, multi->locked_cert_hash_set))
|
|
{
|
|
msg (D_TLS_ERRORS, "TLS Auth Error: TLS object CN=%s client-provided SSL certs unexpectedly changed during mid-session reauth",
|
|
session->common_name);
|
|
|
|
/* disable the tunnel */
|
|
tls_deauthenticate (multi);
|
|
}
|
|
}
|
|
|
|
/* verify --client-config-dir based authentication */
|
|
if (ks->authenticated && session->opt->client_config_dir_exclusive)
|
|
{
|
|
struct gc_arena gc = gc_new ();
|
|
|
|
const char *cn = session->common_name;
|
|
const char *path = gen_path (session->opt->client_config_dir_exclusive, cn, &gc);
|
|
if (!cn || !strcmp (cn, CCD_DEFAULT) || !test_file (path))
|
|
{
|
|
ks->authenticated = false;
|
|
msg (D_TLS_ERRORS, "TLS Auth Error: --client-config-dir authentication failed for common name '%s' file='%s'",
|
|
session->common_name,
|
|
path ? path : "UNDEF");
|
|
}
|
|
|
|
gc_free (&gc);
|
|
}
|
|
}
|
|
#endif /* defined(USE_CRYPTO) && defined(USE_SSL) */
|