Enable DTLS protocol negotiation

The new negotiation is as follows: If the client's X-DTLS-CipherSuite
contains the "PSK-NEGOTIATE" keyword, the server will reply with
"X-DTLS-CipherSuite: PSK-NEGOTIATE" and will enable DTLS-PSK negotiation on the
DTLS channel.

That change utilizes the value provided by sever's X-DTLS-App-ID header
and sets that value to a TLS extension on client hello. The
extension used is defined on (draft-mavrogiannopoulos-app-id).
Signed-off-by: default avatarNikos Mavrogiannopoulos <nmav@redhat.com>
Signed-off-by: default avatarDavid Woodhouse <David.Woodhouse@intel.com>
parent 5060a8cf
......@@ -188,6 +188,26 @@ static void append_mobile_headers(struct openconnect_info *vpninfo, struct oc_te
}
}
static int parse_hex_val(const char *str, unsigned char *storage, unsigned int max_storage_len, int *changed)
{
int len = strlen(str);
unsigned i;
if (len % 2 == 1 || len > 2*max_storage_len) {
return -EINVAL;
}
for (i = 0; i < len; i += 2) {
unsigned char c = unhex(str + i);
if (storage[i/2] != c) {
storage[i/2] = c;
*changed = 1;
}
}
return len/2;
}
static int start_cstp_connection(struct openconnect_info *vpninfo)
{
struct oc_text_buf *reqbuf;
......@@ -242,6 +262,9 @@ static int start_cstp_connection(struct openconnect_info *vpninfo)
buf_append(reqbuf, "X-CSTP-Full-IPv6-Capability: true\r\n");
#ifdef HAVE_DTLS
if (vpninfo->dtls_state != DTLS_DISABLED) {
/* The X-DTLS-Master-Secret is only used for the legacy protocol negotation
* which required the client to send explicitly the secret. In the PSK-NEGOTIATE
* method, the master secret is implicitly agreed on */
buf_append(reqbuf, "X-DTLS-Master-Secret: ");
for (i = 0; i < sizeof(vpninfo->dtls_secret); i++) {
buf_append(reqbuf, "%02X", vpninfo->dtls_secret[i]);
......@@ -381,25 +404,39 @@ static int start_cstp_connection(struct openconnect_info *vpninfo)
mtu = dtlsmtu;
} else if (!strcmp(buf + 7, "Session-ID")) {
int dtls_sessid_changed = 0;
int vsize;
if (strlen(colon) != 64) {
vsize = parse_hex_val(colon, vpninfo->dtls_session_id, sizeof(vpninfo->dtls_session_id), &dtls_sessid_changed);
if (vsize != 32) {
vpn_progress(vpninfo, PRG_ERR,
_("X-DTLS-Session-ID not 64 characters; is: \"%s\"\n"),
colon);
_("X-DTLS-Session-ID not 64 characters; is: \"%s\"\n"),
colon);
vpninfo->dtls_attempt_period = 0;
return -EINVAL;
}
for (i = 0; i < 64; i += 2) {
unsigned char c = unhex(colon + i);
if (vpninfo->dtls_session_id[i/2] != c) {
vpninfo->dtls_session_id[i/2] = c;
dtls_sessid_changed = 1;
}
}
sessid_found = 1;
if (dtls_sessid_changed && vpninfo->dtls_state > DTLS_SLEEPING)
vpninfo->dtls_need_reconnect = 1;
} else if (!strcmp(buf + 7, "App-ID")) {
int dtls_appid_changed = 0;
int vsize;
vsize = parse_hex_val(colon, vpninfo->dtls_app_id, sizeof(vpninfo->dtls_app_id), &dtls_appid_changed);
if (vsize <= 0) {
vpn_progress(vpninfo, PRG_ERR,
_("X-DTLS-Session-ID is invalid; is: \"%s\"\n"),
colon);
vpninfo->dtls_attempt_period = 0;
return -EINVAL;
}
vpninfo->dtls_app_id_size = vsize;
sessid_found = 1;
if (dtls_appid_changed && vpninfo->dtls_state > DTLS_SLEEPING)
vpninfo->dtls_need_reconnect = 1;
} else if (!strcmp(buf + 7, "Content-Encoding")) {
if (!strcmp(colon, "lzs"))
vpninfo->dtls_compr = COMPR_LZS;
......
......@@ -101,6 +101,9 @@ void append_dtls_ciphers(struct openconnect_info *vpninfo, struct oc_text_buf *b
gnutls_priority_t cache;
uint32_t used = 0;
buf_append(buf, "PSK-NEGOTIATE");
first = 0;
ret = gnutls_priority_init(&cache, vpninfo->gnutls_prio, NULL);
if (ret < 0) {
buf->error = -EIO;
......@@ -133,6 +136,113 @@ void append_dtls_ciphers(struct openconnect_info *vpninfo, struct oc_text_buf *b
}
#endif
/* This enables a DTLS protocol negotiation. The new negotiation is as follows:
*
* If the client's X-DTLS-CipherSuite contains the "PSK-NEGOTIATE" keyword,
* the server will reply with "X-DTLS-CipherSuite: PSK-NEGOTIATE" and will
* enable DTLS-PSK negotiation on the DTLS channel. This allows the protocol
* to use new DTLS versions, as well as new DTLS ciphersuites, as long as
* they are also permitted by the system crypto policy in use.
*
* That change still requires to client to pretend it is resuming by setting
* in the TLS ClientHello the session ID provided by the X-DTLS-Session-ID
* header. That is, because there is no TLS extension we can use to set an
* identifier in the client hello (draft-jay-tls-psk-identity-extension
* could be used in the future). The session is not actually resumed.
*/
static int start_dtls_psk_handshake(struct openconnect_info *vpninfo, int dtls_fd)
{
gnutls_session_t dtls_ssl;
gnutls_datum_t key;
struct oc_text_buf *prio;
int err;
prio = buf_alloc();
buf_append(prio, "%s:-VERS-TLS-ALL:+VERS-DTLS-ALL:-KX-ALL:+PSK", vpninfo->gnutls_prio);
if (buf_error(prio)) {
vpn_progress(vpninfo, PRG_ERR,
_("Failed to generate DTLS priority string\n"));
vpninfo->dtls_attempt_period = 0;
return buf_free(prio);
}
err = gnutls_init(&dtls_ssl, GNUTLS_CLIENT|GNUTLS_DATAGRAM|GNUTLS_NONBLOCK);
if (err) {
vpn_progress(vpninfo, PRG_ERR,
_("Failed to initialize DTLS: %s\n"),
gnutls_strerror(err));
goto fail;
}
gnutls_session_set_ptr(dtls_ssl, (void *) vpninfo);
err = gnutls_priority_set_direct(dtls_ssl, prio->data, NULL);
if (err) {
vpn_progress(vpninfo, PRG_ERR,
_("Failed to set DTLS priority: '%s': %s\n"),
prio->data, gnutls_strerror(err));
goto fail;
}
gnutls_transport_set_ptr(dtls_ssl,
(gnutls_transport_ptr_t)(intptr_t)dtls_fd);
/* set PSK credentials */
err = gnutls_psk_allocate_client_credentials(&vpninfo->psk_cred);
if (err < 0) {
vpn_progress(vpninfo, PRG_ERR,
_("Failed to allocate credentials: %s\n"),
gnutls_strerror(err));
goto fail;
}
/* generate key */
/* we should have used gnutls_prf_rfc5705() but since we don't use
* the RFC5705 context, the output is identical with gnutls_prf(). The
* latter is available in much earlier versions of gnutls. */
err = gnutls_prf(vpninfo->https_sess, PSK_LABEL_SIZE, PSK_LABEL,
0, 0, 0, PSK_KEY_SIZE, (char*)vpninfo->dtls_secret);
if (err < 0) {
vpn_progress(vpninfo, PRG_ERR,
_("Failed to generate DTLS key: %s\n"),
gnutls_strerror(err));
goto fail;
}
key.data = vpninfo->dtls_secret;
key.size = PSK_KEY_SIZE;
/* we set an arbitrary username here. We cannot take advantage of the
* username field to send our ID to the server, since the username in TLS-PSK
* is sent after the server-hello. */
err = gnutls_psk_set_client_credentials(vpninfo->psk_cred, "psk", &key, 0);
if (err < 0) {
vpn_progress(vpninfo, PRG_ERR,
_("Failed to set DTLS key: %s\n"),
gnutls_strerror(err));
goto fail;
}
err = gnutls_credentials_set(dtls_ssl, GNUTLS_CRD_PSK, vpninfo->psk_cred);
if (err) {
vpn_progress(vpninfo, PRG_ERR,
_("Failed to set DTLS PSK credentials: %s\n"),
gnutls_strerror(err));
goto fail;
}
buf_free(prio);
vpninfo->dtls_ssl = dtls_ssl;
return 0;
fail:
buf_free(prio);
gnutls_deinit(dtls_ssl);
gnutls_psk_free_client_credentials(vpninfo->psk_cred);
vpninfo->psk_cred = NULL;
vpninfo->dtls_attempt_period = 0;
return -EINVAL;
}
int start_dtls_handshake(struct openconnect_info *vpninfo, int dtls_fd)
{
gnutls_session_t dtls_ssl;
......@@ -140,6 +250,9 @@ int start_dtls_handshake(struct openconnect_info *vpninfo, int dtls_fd)
int err;
int cipher;
if (strcmp(vpninfo->dtls_cipher, "PSK-NEGOTIATE") == 0)
return start_dtls_psk_handshake(vpninfo, dtls_fd);
for (cipher = 0; cipher < sizeof(gnutls_dtls_ciphers)/sizeof(gnutls_dtls_ciphers[0]); cipher++) {
if (gnutls_check_version(gnutls_dtls_ciphers[cipher].min_gnutls_version) == NULL)
continue;
......@@ -154,6 +267,8 @@ int start_dtls_handshake(struct openconnect_info *vpninfo, int dtls_fd)
found_cipher:
gnutls_init(&dtls_ssl, GNUTLS_CLIENT|GNUTLS_DATAGRAM|GNUTLS_NONBLOCK);
gnutls_session_set_ptr(dtls_ssl, (void *) vpninfo);
err = gnutls_priority_set_direct(dtls_ssl,
gnutls_dtls_ciphers[cipher].prio,
NULL);
......@@ -266,4 +381,9 @@ void dtls_shutdown(struct openconnect_info *vpninfo)
void dtls_ssl_free(struct openconnect_info *vpninfo)
{
gnutls_deinit(vpninfo->dtls_ssl);
if (vpninfo->psk_cred) {
gnutls_psk_free_client_credentials(vpninfo->psk_cred);
vpninfo->psk_cred = NULL;
}
}
......@@ -2539,6 +2539,117 @@ void openconnect_close_https(struct openconnect_info *vpninfo, int final)
}
}
#if GNUTLS_VERSION_NUMBER >= 0x030400
static int ext_recv_client(gnutls_session_t sess, const unsigned char *buf, size_t buflen)
{
/* we shouldn't have received that */
return 0;
}
static int ext_send_client(gnutls_session_t sess, gnutls_buffer_t extdata)
{
struct openconnect_info *vpninfo = gnutls_session_get_ptr(sess);
if (vpninfo->dtls_ssl != sess)
return 0;
if (vpninfo->dtls_app_id_size > 0) {
uint8_t size = vpninfo->dtls_app_id_size;
int ret;
ret = gnutls_buffer_append_data(extdata, &size, 1);
if (ret < 0)
return ret;
ret = gnutls_buffer_append_data(extdata, vpninfo->dtls_app_id, vpninfo->dtls_app_id_size);
if (ret < 0)
return ret;
return vpninfo->dtls_app_id_size + 1;
}
return 0;
}
#else
/* previously to 3.4.0 we can only use internal-but-exported APIs
*/
typedef int (*gnutls_ext_recv_func) (gnutls_session_t session,
const unsigned char *data,
size_t len);
typedef int (*gnutls_ext_send_func) (gnutls_session_t session,
void* extdata);
int _gnutls_buffer_append_data(void *, const void *data, size_t data_size);
typedef struct {
const char *name;
uint16_t type;
int parse_type;
/* this function must return 0 when Not Applicable
* size of extension data if ok
* < 0 on other error.
*/
gnutls_ext_recv_func recv_func;
/* this function must return 0 when Not Applicable
* size of extension data if ok
* GNUTLS_E_INT_RET_0 if extension data size is zero
* < 0 on other error.
*/
gnutls_ext_send_func send_func;
void *deinit_func; /* this will be called to deinitialize
* internal data
*/
void *pack_func; /* packs internal data to machine independent format */
void *unpack_func; /* unpacks internal data */
void *epoch_func; /* called after the handshake is finished */
} extension_entry_st;
int _gnutls_ext_register(extension_entry_st *);
static int ext_recv_client(gnutls_session_t sess, const unsigned char *buf, size_t buflen)
{
/* we shouldn't have received that */
return 0;
}
static int ext_send_client(gnutls_session_t sess, void *extdata)
{
struct openconnect_info *vpninfo = gnutls_session_get_ptr(sess);
if (vpninfo->dtls_ssl != sess)
return 0;
if (vpninfo->dtls_app_id_size > 0) {
uint8_t size = vpninfo->dtls_app_id_size;
int ret;
ret = _gnutls_buffer_append_data(extdata, &size, 1);
if (ret < 0)
return ret;
ret = _gnutls_buffer_append_data(extdata, vpninfo->dtls_app_id, vpninfo->dtls_app_id_size);
if (ret < 0)
return ret;
return vpninfo->dtls_app_id_size + 1;
}
return 0;
}
extension_entry_st ext_app_id = {
.name = "app-id",
.type = DTLS_APP_ID_EXT,
.parse_type = 2,
.recv_func = ext_recv_client,
.send_func = ext_send_client,
.pack_func = NULL,
.unpack_func = NULL,
.deinit_func = NULL
};
#endif
int openconnect_init_ssl(void)
{
#ifdef _WIN32
......@@ -2549,6 +2660,12 @@ int openconnect_init_ssl(void)
if (gnutls_global_init())
return -EIO;
#if GNUTLS_VERSION_NUMBER >= 0x030400
gnutls_ext_register("APP-ID", DTLS_APP_ID_EXT, GNUTLS_EXT_TLS, ext_recv_client, ext_send_client, NULL, NULL, NULL);
#else
_gnutls_ext_register(&ext_app_id);
#endif
return 0;
}
......
......@@ -175,6 +175,8 @@ struct pkt {
#endif
#define COMPR_ALL (COMPR_STATELESS | COMPR_DEFLATE)
#define DTLS_APP_ID_EXT 48018
struct keepalive_info {
int dpd;
int keepalive;
......@@ -483,6 +485,7 @@ struct openconnect_info {
#elif defined(OPENCONNECT_GNUTLS)
gnutls_session_t https_sess;
gnutls_certificate_credentials_t https_cred;
gnutls_psk_client_credentials_t psk_cred;
char local_cert_md5[MD5_SIZE * 2 + 1]; /* For CSD */
char gnutls_prio[256];
#ifdef HAVE_TROUSERS
......@@ -546,6 +549,8 @@ struct openconnect_info {
struct keepalive_info dtls_times;
unsigned char dtls_session_id[32];
unsigned char dtls_secret[48];
unsigned char dtls_app_id[32];
unsigned dtls_app_id_size;
char *dtls_cipher;
char *vpnc_script;
......@@ -664,6 +669,11 @@ struct openconnect_info {
#define read_fd_monitored(_v, _n) FD_ISSET(_v->_n##_fd, &_v->_select_rfds)
#endif
/* Key material for DTLS-PSK */
#define PSK_LABEL "EXPORTER-openconnect-psk"
#define PSK_LABEL_SIZE sizeof(PSK_LABEL)-1
#define PSK_KEY_SIZE 32
/* Packet types */
#define AC_PKT_DATA 0 /* Uncompressed data */
......
......@@ -176,6 +176,58 @@ static SSL_SESSION *generate_dtls_session(struct openconnect_info *vpninfo,
}
#endif
#if defined (HAVE_DTLS12) && !defined(OPENSSL_NO_PSK)
static unsigned int psk_callback(SSL *ssl, const char *hint, char *identity,
unsigned int max_identity_len, unsigned char *psk,
unsigned int max_psk_len)
{
struct openconnect_info *vpninfo = SSL_get_app_data(ssl);
if (!vpninfo || max_identity_len < 4 || max_psk_len < PSK_KEY_SIZE)
return 0;
vpn_progress(vpninfo, PRG_TRACE, _("PSK callback\n"));
snprintf(identity, max_psk_len, "psk");
memcpy(psk, vpninfo->dtls_secret, PSK_KEY_SIZE);
return PSK_KEY_SIZE;
}
static int pskident_add(SSL *s, unsigned int ext_type, const unsigned char **out, size_t *outlen,
int *al, void *add_arg)
{
struct openconnect_info *vpninfo = add_arg;
unsigned char *buf;
buf = malloc(vpninfo->dtls_app_id_size + 1);
if (!buf) {
vpn_progress(vpninfo, PRG_ERR,
_("Failed to create app-identity extension for OpenSSL\n"));
return 0;
}
buf[0] = vpninfo->dtls_app_id_size;
memcpy(&buf[1], vpninfo->dtls_app_id, vpninfo->dtls_app_id_size);
*out = buf;
*outlen = vpninfo->dtls_app_id_size + 1;
return 1;
}
static void pskident_free(SSL *s, unsigned int ext_type, const unsigned char *out, void *add_arg)
{
free((void *)out);
}
static int pskident_parse(SSL *s, unsigned int ext_type, const unsigned char *in, size_t inlen,
int *al, void *parse_arg)
{
return 1;
}
#endif
int start_dtls_handshake(struct openconnect_info *vpninfo, int dtls_fd)
{
STACK_OF(SSL_CIPHER) *ciphers;
......@@ -193,6 +245,10 @@ int start_dtls_handshake(struct openconnect_info *vpninfo, int dtls_fd)
} else if (!strcmp(cipher, "OC-DTLS1_2-AES256-GCM")) {
dtlsver = DTLS1_2_VERSION;
cipher = "AES256-GCM-SHA384";
#ifndef OPENSSL_NO_PSK
} else if (!strcmp(cipher, "PSK-NEGOTIATE")) {
dtlsver = 0; /* Let it negotiate */
#endif
}
#endif
......@@ -215,22 +271,45 @@ int start_dtls_handshake(struct openconnect_info *vpninfo, int dtls_fd)
vpninfo->dtls_attempt_period = 0;
return -EINVAL;
}
if (dtlsver) {
#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
if (dtlsver == DTLS1_BAD_VER)
SSL_CTX_set_options(vpninfo->dtls_ctx, SSL_OP_CISCO_ANYCONNECT);
if (dtlsver == DTLS1_BAD_VER)
SSL_CTX_set_options(vpninfo->dtls_ctx, SSL_OP_CISCO_ANYCONNECT);
#else
if (!SSL_CTX_set_min_proto_version(vpninfo->dtls_ctx, dtlsver) ||
!SSL_CTX_set_max_proto_version(vpninfo->dtls_ctx, dtlsver)) {
vpn_progress(vpninfo, PRG_ERR,
_("Set DTLS CTX version failed\n"));
openconnect_report_ssl_errors(vpninfo);
SSL_CTX_free(vpninfo->dtls_ctx);
vpninfo->dtls_ctx = NULL;
vpninfo->dtls_attempt_period = 0;
return -EINVAL;
}
if (!SSL_CTX_set_min_proto_version(vpninfo->dtls_ctx, dtlsver) ||
!SSL_CTX_set_max_proto_version(vpninfo->dtls_ctx, dtlsver)) {
vpn_progress(vpninfo, PRG_ERR,
_("Set DTLS CTX version failed\n"));
openconnect_report_ssl_errors(vpninfo);
SSL_CTX_free(vpninfo->dtls_ctx);
vpninfo->dtls_ctx = NULL;
vpninfo->dtls_attempt_period = 0;
return -EINVAL;
}
#endif
#if defined (HAVE_DTLS12) && !defined(OPENSSL_NO_PSK)
} else {
SSL_CTX_set_psk_client_callback(vpninfo->dtls_ctx, psk_callback);
/* For PSK we override the DTLS master secret with one derived
* from the HTTPS session. */
if (!SSL_export_keying_material(vpninfo->https_ssl,
vpninfo->dtls_secret, PSK_KEY_SIZE,
PSK_LABEL, PSK_LABEL_SIZE, NULL, 0, 0)) {
vpn_progress(vpninfo, PRG_ERR,
_("Failed to generate DTLS key\n"));
openconnect_report_ssl_errors(vpninfo);
SSL_CTX_free(vpninfo->dtls_ctx);
vpninfo->dtls_ctx = NULL;
vpninfo->dtls_attempt_period = 0;
return -EINVAL;
}
SSL_CTX_add_client_custom_ext(vpninfo->dtls_ctx, DTLS_APP_ID_EXT,
pskident_add, pskident_free, vpninfo,
pskident_parse, vpninfo);
/* For SSL_CTX_set_cipher_list() */
cipher = "PSK";
#endif
}
/* If we don't readahead, then we do short reads and throw
away the tail of data packets. */
SSL_CTX_set_read_ahead(vpninfo->dtls_ctx, 1);
......@@ -247,9 +326,10 @@ int start_dtls_handshake(struct openconnect_info *vpninfo, int dtls_fd)
dtls_ssl = SSL_new(vpninfo->dtls_ctx);
SSL_set_connect_state(dtls_ssl);
SSL_set_app_data(dtls_ssl, vpninfo);
ciphers = SSL_get_ciphers(dtls_ssl);
if (sk_SSL_CIPHER_num(ciphers) != 1) {
if (dtlsver != 0 && sk_SSL_CIPHER_num(ciphers) != 1) {
vpn_progress(vpninfo, PRG_ERR, _("Not precisely one DTLS cipher\n"));
SSL_CTX_free(vpninfo->dtls_ctx);
SSL_free(dtls_ssl);
......@@ -258,6 +338,17 @@ int start_dtls_handshake(struct openconnect_info *vpninfo, int dtls_fd)
return -EINVAL;
}
#if defined (HAVE_DTLS12) && !defined(OPENSSL_NO_PSK)
/* In the PSK case, OpenSSL 1.1+ will negotiate properly regardless of
* this. But OpenSSL = 1.0.2 will do precisely the version requested
* here. Which we don't want because we *want* it to negotiate. The
* session we're pretending to resume is *only* to let the server know
* who we are, since draft-jay-tls-psk-identify-extension isn't here
* yet. */
if (!dtlsver)
dtlsver = DTLS1_2_VERSION;
#endif
/* We're going to "resume" a session which never existed. Fake it... */
dtls_session = generate_dtls_session(vpninfo, dtlsver,
sk_SSL_CIPHER_value(ciphers, 0));
......@@ -418,15 +509,18 @@ void dtls_shutdown(struct openconnect_info *vpninfo)
void dtls_ssl_free(struct openconnect_info *vpninfo)
{
/* We are only ever called when this is non-NULL */
SSL_free(vpninfo->dtls_ssl);
}
void append_dtls_ciphers(struct openconnect_info *vpninfo, struct oc_text_buf *buf)
{
#ifdef HAVE_DTLS12
buf_append(buf, "OC-DTLS1_2-AES256-GCM:OC-DTLS1_2-AES128-GCM:AES256-SHA:AES128-SHA:DES-CBC3-SHA:DES-CBC-SHA");
#else
buf_append(buf, "AES256-SHA:AES128-SHA:DES-CBC3-SHA:DES-CBC-SHA");
#ifndef OPENSSL_NO_PSK
buf_append(buf, "PSK-NEGOTIATE:");
#endif
buf_append(buf, "OC-DTLS1_2-AES256-GCM:OC-DTLS1_2-AES128-GCM:");
#endif
buf_append(buf, "AES256-SHA:AES128-SHA:DES-CBC3-SHA:DES-CBC-SHA");
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment