mirror of
https://github.com/curl/curl.git
synced 2024-11-27 05:50:21 +08:00
758f6eed51
(http://curl.haxx.se/bug/view.cgi?id=1480821) He found and identified a problem with how libcurl dealt with GnuTLS and a case where gnutls returned GNUTLS_E_AGAIN indicating it would block. It would then return an unexpected return code, making Curl_ssl_send() confuse the upper layer - causing random 28 bytes trash data to get inserted in the transfered stream. The proper fix was to make the Curl_gtls_send() function return the proper return codes that the callers would expect. The Curl_ossl_send() function already did this.
550 lines
16 KiB
C
550 lines
16 KiB
C
/***************************************************************************
|
|
* _ _ ____ _
|
|
* Project ___| | | | _ \| |
|
|
* / __| | | | |_) | |
|
|
* | (__| |_| | _ <| |___
|
|
* \___|\___/|_| \_\_____|
|
|
*
|
|
* Copyright (C) 1998 - 2005, Daniel Stenberg, <daniel@haxx.se>, et al.
|
|
*
|
|
* This software is licensed as described in the file COPYING, which
|
|
* you should have received as part of this distribution. The terms
|
|
* are also available at http://curl.haxx.se/docs/copyright.html.
|
|
*
|
|
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
|
|
* copies of the Software, and permit persons to whom the Software is
|
|
* furnished to do so, under the terms of the COPYING file.
|
|
*
|
|
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
|
|
* KIND, either express or implied.
|
|
*
|
|
* $Id$
|
|
***************************************************************************/
|
|
|
|
/*
|
|
* Source file for all GnuTLS-specific code for the TLS/SSL layer. No code
|
|
* but sslgen.c should ever call or use these functions.
|
|
*
|
|
* Note: don't use the GnuTLS' *_t variable type names in this source code,
|
|
* since they were not present in 1.0.X.
|
|
*/
|
|
|
|
#include "setup.h"
|
|
#ifdef USE_GNUTLS
|
|
#include <gnutls/gnutls.h>
|
|
#include <gnutls/x509.h>
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <ctype.h>
|
|
#ifdef HAVE_SYS_TYPES_H
|
|
#include <sys/types.h>
|
|
#endif
|
|
#ifdef HAVE_SYS_SOCKET_H
|
|
#include <sys/socket.h>
|
|
#endif
|
|
|
|
#include "urldata.h"
|
|
#include "sendf.h"
|
|
#include "gtls.h"
|
|
#include "sslgen.h"
|
|
#include "parsedate.h"
|
|
#include "connect.h" /* for the connect timeout */
|
|
#include "select.h"
|
|
#define _MPRINTF_REPLACE /* use our functions only */
|
|
#include <curl/mprintf.h>
|
|
#include "memory.h"
|
|
/* The last #include file should be: */
|
|
#include "memdebug.h"
|
|
|
|
/* Enable GnuTLS debugging by defining GTLSDEBUG */
|
|
/*#define GTLSDEBUG */
|
|
|
|
#ifdef GTLSDEBUG
|
|
static void tls_log_func(int level, const char *str)
|
|
{
|
|
fprintf(stderr, "|<%d>| %s", level, str);
|
|
}
|
|
#endif
|
|
|
|
|
|
/* Global GnuTLS init, called from Curl_ssl_init() */
|
|
int Curl_gtls_init(void)
|
|
{
|
|
gnutls_global_init();
|
|
#ifdef GTLSDEBUG
|
|
gnutls_global_set_log_function(tls_log_func);
|
|
gnutls_global_set_log_level(2);
|
|
#endif
|
|
return 1;
|
|
}
|
|
|
|
int Curl_gtls_cleanup(void)
|
|
{
|
|
gnutls_global_deinit();
|
|
return 1;
|
|
}
|
|
|
|
static void showtime(struct SessionHandle *data,
|
|
const char *text,
|
|
time_t stamp)
|
|
{
|
|
struct tm *tm;
|
|
#ifdef HAVE_GMTIME_R
|
|
struct tm buffer;
|
|
tm = (struct tm *)gmtime_r(&stamp, &buffer);
|
|
#else
|
|
tm = gmtime(&stamp);
|
|
#endif
|
|
snprintf(data->state.buffer,
|
|
BUFSIZE,
|
|
"\t %s: %s, %02d %s %4d %02d:%02d:%02d GMT\n",
|
|
text,
|
|
Curl_wkday[tm->tm_wday?tm->tm_wday-1:6],
|
|
tm->tm_mday,
|
|
Curl_month[tm->tm_mon],
|
|
tm->tm_year + 1900,
|
|
tm->tm_hour,
|
|
tm->tm_min,
|
|
tm->tm_sec);
|
|
infof(data, "%s", data->state.buffer);
|
|
}
|
|
|
|
/* this function does a BLOCKING SSL/TLS (re-)handshake */
|
|
static CURLcode handshake(struct connectdata *conn,
|
|
gnutls_session session,
|
|
int sockindex,
|
|
bool duringconnect)
|
|
{
|
|
struct SessionHandle *data = conn->data;
|
|
int rc;
|
|
|
|
do {
|
|
rc = gnutls_handshake(session);
|
|
|
|
if((rc == GNUTLS_E_AGAIN) || (rc == GNUTLS_E_INTERRUPTED)) {
|
|
long timeout_ms = DEFAULT_CONNECT_TIMEOUT;
|
|
long has_passed;
|
|
|
|
if(duringconnect && data->set.connecttimeout)
|
|
timeout_ms = data->set.connecttimeout*1000;
|
|
|
|
if(data->set.timeout) {
|
|
/* get the strictest timeout of the ones converted to milliseconds */
|
|
if((data->set.timeout*1000) < timeout_ms)
|
|
timeout_ms = data->set.timeout*1000;
|
|
}
|
|
|
|
/* Evaluate in milliseconds how much time that has passed */
|
|
has_passed = Curl_tvdiff(Curl_tvnow(), data->progress.t_startsingle);
|
|
|
|
/* subtract the passed time */
|
|
timeout_ms -= has_passed;
|
|
|
|
if(timeout_ms < 0) {
|
|
/* a precaution, no need to continue if time already is up */
|
|
failf(data, "SSL connection timeout");
|
|
return CURLE_OPERATION_TIMEOUTED;
|
|
}
|
|
|
|
rc = Curl_select(conn->sock[sockindex],
|
|
conn->sock[sockindex], (int)timeout_ms);
|
|
if(rc > 0)
|
|
/* reabable or writable, go loop*/
|
|
continue;
|
|
else if(0 == rc) {
|
|
/* timeout */
|
|
failf(data, "SSL connection timeout");
|
|
return CURLE_OPERATION_TIMEDOUT;
|
|
}
|
|
else {
|
|
/* anything that gets here is fatally bad */
|
|
failf(data, "select on SSL socket, errno: %d", Curl_ourerrno());
|
|
return CURLE_SSL_CONNECT_ERROR;
|
|
}
|
|
}
|
|
else
|
|
break;
|
|
} while(1);
|
|
|
|
if (rc < 0) {
|
|
failf(data, "gnutls_handshake() failed: %d", rc);
|
|
/* gnutls_perror(ret); */
|
|
return CURLE_SSL_CONNECT_ERROR;
|
|
}
|
|
|
|
return CURLE_OK;
|
|
}
|
|
|
|
static gnutls_x509_crt_fmt do_file_type(const char *type)
|
|
{
|
|
if(!type || !type[0])
|
|
return GNUTLS_X509_FMT_PEM;
|
|
if(curl_strequal(type, "PEM"))
|
|
return GNUTLS_X509_FMT_PEM;
|
|
if(curl_strequal(type, "DER"))
|
|
return GNUTLS_X509_FMT_DER;
|
|
return -1;
|
|
}
|
|
|
|
|
|
/*
|
|
* This function is called after the TCP connect has completed. Setup the TLS
|
|
* layer and do all necessary magic.
|
|
*/
|
|
CURLcode
|
|
Curl_gtls_connect(struct connectdata *conn,
|
|
int sockindex)
|
|
|
|
{
|
|
const int cert_type_priority[] = { GNUTLS_CRT_X509, 0 };
|
|
struct SessionHandle *data = conn->data;
|
|
gnutls_session session;
|
|
int rc;
|
|
unsigned int cert_list_size;
|
|
const gnutls_datum *chainp;
|
|
unsigned int verify_status;
|
|
gnutls_x509_crt x509_cert;
|
|
char certbuf[256]; /* big enough? */
|
|
size_t size;
|
|
unsigned int algo;
|
|
unsigned int bits;
|
|
time_t clock;
|
|
const char *ptr;
|
|
void *ssl_sessionid;
|
|
size_t ssl_idsize;
|
|
|
|
/* GnuTLS only supports TLSv1 (and SSLv3?) */
|
|
if(data->set.ssl.version == CURL_SSLVERSION_SSLv2) {
|
|
failf(data, "GnuTLS does not support SSLv2");
|
|
return CURLE_SSL_CONNECT_ERROR;
|
|
}
|
|
|
|
/* allocate a cred struct */
|
|
rc = gnutls_certificate_allocate_credentials(&conn->ssl[sockindex].cred);
|
|
if(rc < 0) {
|
|
failf(data, "gnutls_cert_all_cred() failed: %s", gnutls_strerror(rc));
|
|
return CURLE_SSL_CONNECT_ERROR;
|
|
}
|
|
|
|
if(data->set.ssl.CAfile) {
|
|
/* set the trusted CA cert bundle file */
|
|
gnutls_certificate_set_verify_flags(conn->ssl[sockindex].cred,
|
|
GNUTLS_VERIFY_ALLOW_X509_V1_CA_CRT);
|
|
|
|
rc = gnutls_certificate_set_x509_trust_file(conn->ssl[sockindex].cred,
|
|
data->set.ssl.CAfile,
|
|
GNUTLS_X509_FMT_PEM);
|
|
if(rc < 0)
|
|
infof(data, "error reading ca cert file %s (%s)\n",
|
|
data->set.ssl.CAfile, gnutls_strerror(rc));
|
|
else
|
|
infof(data, "found %d certificates in %s\n",
|
|
rc, data->set.ssl.CAfile);
|
|
}
|
|
|
|
/* Initialize TLS session as a client */
|
|
rc = gnutls_init(&conn->ssl[sockindex].session, GNUTLS_CLIENT);
|
|
if(rc) {
|
|
failf(data, "gnutls_init() failed: %d", rc);
|
|
return CURLE_SSL_CONNECT_ERROR;
|
|
}
|
|
|
|
/* convenient assign */
|
|
session = conn->ssl[sockindex].session;
|
|
|
|
/* Use default priorities */
|
|
rc = gnutls_set_default_priority(session);
|
|
if(rc < 0)
|
|
return CURLE_SSL_CONNECT_ERROR;
|
|
|
|
/* Sets the priority on the certificate types supported by gnutls. Priority
|
|
is higher for types specified before others. After specifying the types
|
|
you want, you must append a 0. */
|
|
rc = gnutls_certificate_type_set_priority(session, cert_type_priority);
|
|
if(rc < 0)
|
|
return CURLE_SSL_CONNECT_ERROR;
|
|
|
|
if(data->set.cert) {
|
|
if( gnutls_certificate_set_x509_key_file(
|
|
conn->ssl[sockindex].cred, data->set.cert,
|
|
data->set.key != 0 ? data->set.key : data->set.cert,
|
|
do_file_type(data->set.cert_type) ) ) {
|
|
failf(data, "error reading X.509 key or certificate file");
|
|
return CURLE_SSL_CONNECT_ERROR;
|
|
}
|
|
}
|
|
|
|
/* put the credentials to the current session */
|
|
rc = gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE,
|
|
conn->ssl[sockindex].cred);
|
|
|
|
/* set the connection handle (file descriptor for the socket) */
|
|
gnutls_transport_set_ptr(session,
|
|
(gnutls_transport_ptr)conn->sock[sockindex]);
|
|
|
|
/* This might be a reconnect, so we check for a session ID in the cache
|
|
to speed up things */
|
|
|
|
if(!Curl_ssl_getsessionid(conn, &ssl_sessionid, &ssl_idsize)) {
|
|
/* we got a session id, use it! */
|
|
gnutls_session_set_data(session, ssl_sessionid, ssl_idsize);
|
|
|
|
/* Informational message */
|
|
infof (data, "SSL re-using session ID\n");
|
|
}
|
|
|
|
rc = handshake(conn, session, sockindex, TRUE);
|
|
if(rc)
|
|
/* handshake() sets its own error message with failf() */
|
|
return rc;
|
|
|
|
/* This function will return the peer's raw certificate (chain) as sent by
|
|
the peer. These certificates are in raw format (DER encoded for
|
|
X.509). In case of a X.509 then a certificate list may be present. The
|
|
first certificate in the list is the peer's certificate, following the
|
|
issuer's certificate, then the issuer's issuer etc. */
|
|
|
|
chainp = gnutls_certificate_get_peers(session, &cert_list_size);
|
|
if(!chainp) {
|
|
if(data->set.ssl.verifyhost) {
|
|
failf(data, "failed to get server cert");
|
|
return CURLE_SSL_PEER_CERTIFICATE;
|
|
}
|
|
infof(data, "\t common name: WARNING couldn't obtain\n");
|
|
}
|
|
|
|
/* This function will try to verify the peer's certificate and return its
|
|
status (trusted, invalid etc.). The value of status should be one or more
|
|
of the gnutls_certificate_status_t enumerated elements bitwise or'd. To
|
|
avoid denial of service attacks some default upper limits regarding the
|
|
certificate key size and chain size are set. To override them use
|
|
gnutls_certificate_set_verify_limits(). */
|
|
|
|
rc = gnutls_certificate_verify_peers2(session, &verify_status);
|
|
if (rc < 0) {
|
|
failf(data, "server cert verify failed: %d", rc);
|
|
return CURLE_SSL_CONNECT_ERROR;
|
|
}
|
|
|
|
/* verify_status is a bitmask of gnutls_certificate_status bits */
|
|
if(verify_status & GNUTLS_CERT_INVALID) {
|
|
if (data->set.ssl.verifypeer) {
|
|
failf(data, "server certificate verification failed. CAfile: %s",
|
|
data->set.ssl.CAfile?data->set.ssl.CAfile:"none");
|
|
return CURLE_SSL_CACERT;
|
|
}
|
|
else
|
|
infof(data, "\t server certificate verification FAILED\n");
|
|
}
|
|
else
|
|
infof(data, "\t server certificate verification OK\n");
|
|
|
|
/* initialize an X.509 certificate structure. */
|
|
gnutls_x509_crt_init(&x509_cert);
|
|
|
|
/* convert the given DER or PEM encoded Certificate to the native
|
|
gnutls_x509_crt_t format */
|
|
gnutls_x509_crt_import(x509_cert, chainp, GNUTLS_X509_FMT_DER);
|
|
|
|
size=sizeof(certbuf);
|
|
rc = gnutls_x509_crt_get_dn_by_oid(x509_cert, GNUTLS_OID_X520_COMMON_NAME,
|
|
0, /* the first and only one */
|
|
FALSE,
|
|
certbuf,
|
|
&size);
|
|
if(rc) {
|
|
infof(data, "error fetching CN from cert:%s\n",
|
|
gnutls_strerror(rc));
|
|
}
|
|
|
|
/* This function will check if the given certificate's subject matches the
|
|
given hostname. This is a basic implementation of the matching described
|
|
in RFC2818 (HTTPS), which takes into account wildcards, and the subject
|
|
alternative name PKIX extension. Returns non zero on success, and zero on
|
|
failure. */
|
|
rc = gnutls_x509_crt_check_hostname(x509_cert, conn->host.name);
|
|
|
|
if(!rc) {
|
|
if (data->set.ssl.verifyhost > 1) {
|
|
failf(data, "SSL: certificate subject name (%s) does not match "
|
|
"target host name '%s'", certbuf, conn->host.dispname);
|
|
gnutls_x509_crt_deinit(x509_cert);
|
|
return CURLE_SSL_PEER_CERTIFICATE;
|
|
}
|
|
else
|
|
infof(data, "\t common name: %s (does not match '%s')\n",
|
|
certbuf, conn->host.dispname);
|
|
}
|
|
else
|
|
infof(data, "\t common name: %s (matched)\n", certbuf);
|
|
|
|
/* Show:
|
|
|
|
- ciphers used
|
|
- subject
|
|
- start date
|
|
- expire date
|
|
- common name
|
|
- issuer
|
|
|
|
*/
|
|
|
|
/* public key algorithm's parameters */
|
|
algo = gnutls_x509_crt_get_pk_algorithm(x509_cert, &bits);
|
|
infof(data, "\t certificate public key: %s\n",
|
|
gnutls_pk_algorithm_get_name(algo));
|
|
|
|
/* version of the X.509 certificate. */
|
|
infof(data, "\t certificate version: #%d\n",
|
|
gnutls_x509_crt_get_version(x509_cert));
|
|
|
|
|
|
size = sizeof(certbuf);
|
|
gnutls_x509_crt_get_dn(x509_cert, certbuf, &size);
|
|
infof(data, "\t subject: %s\n", certbuf);
|
|
|
|
clock = gnutls_x509_crt_get_activation_time(x509_cert);
|
|
showtime(data, "start date", clock);
|
|
|
|
clock = gnutls_x509_crt_get_expiration_time(x509_cert);
|
|
showtime(data, "expire date", clock);
|
|
|
|
size = sizeof(certbuf);
|
|
gnutls_x509_crt_get_issuer_dn(x509_cert, certbuf, &size);
|
|
infof(data, "\t issuer: %s\n", certbuf);
|
|
|
|
gnutls_x509_crt_deinit(x509_cert);
|
|
|
|
/* compression algorithm (if any) */
|
|
ptr = gnutls_compression_get_name(gnutls_compression_get(session));
|
|
/* the *_get_name() says "NULL" if GNUTLS_COMP_NULL is returned */
|
|
infof(data, "\t compression: %s\n", ptr);
|
|
|
|
/* the name of the cipher used. ie 3DES. */
|
|
ptr = gnutls_cipher_get_name(gnutls_cipher_get(session));
|
|
infof(data, "\t cipher: %s\n", ptr);
|
|
|
|
/* the MAC algorithms name. ie SHA1 */
|
|
ptr = gnutls_mac_get_name(gnutls_mac_get(session));
|
|
infof(data, "\t MAC: %s\n", ptr);
|
|
|
|
if(!ssl_sessionid) {
|
|
/* this session was not previously in the cache, add it now */
|
|
|
|
/* get the session ID data size */
|
|
gnutls_session_get_data(session, NULL, &ssl_idsize);
|
|
ssl_sessionid = malloc(ssl_idsize); /* get a buffer for it */
|
|
|
|
if(ssl_sessionid) {
|
|
/* extract session ID to the allocated buffer */
|
|
gnutls_session_get_data(session, ssl_sessionid, &ssl_idsize);
|
|
|
|
/* store this session id */
|
|
return Curl_ssl_addsessionid(conn, ssl_sessionid, ssl_idsize);
|
|
}
|
|
}
|
|
|
|
return CURLE_OK;
|
|
}
|
|
|
|
|
|
/* return number of sent (non-SSL) bytes */
|
|
int Curl_gtls_send(struct connectdata *conn,
|
|
int sockindex,
|
|
void *mem,
|
|
size_t len)
|
|
{
|
|
int rc;
|
|
rc = gnutls_record_send(conn->ssl[sockindex].session, mem, len);
|
|
|
|
if(rc < 0 ) {
|
|
if(rc == GNUTLS_E_AGAIN)
|
|
return 0; /* EWOULDBLOCK equivalent */
|
|
rc = -1; /* generic error code for send failure */
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
void Curl_gtls_close_all(struct SessionHandle *data)
|
|
{
|
|
/* FIX: make the OpenSSL code more generic and use parts of it here */
|
|
(void)data;
|
|
}
|
|
|
|
static void close_one(struct connectdata *conn,
|
|
int index)
|
|
{
|
|
if(conn->ssl[index].session) {
|
|
gnutls_bye(conn->ssl[index].session, GNUTLS_SHUT_RDWR);
|
|
gnutls_deinit(conn->ssl[index].session);
|
|
}
|
|
gnutls_certificate_free_credentials(conn->ssl[index].cred);
|
|
}
|
|
|
|
void Curl_gtls_close(struct connectdata *conn)
|
|
{
|
|
if(conn->ssl[0].use)
|
|
close_one(conn, 0);
|
|
if(conn->ssl[1].use)
|
|
close_one(conn, 1);
|
|
}
|
|
|
|
/*
|
|
* If the read would block we return -1 and set 'wouldblock' to TRUE.
|
|
* Otherwise we return the amount of data read. Other errors should return -1
|
|
* and set 'wouldblock' to FALSE.
|
|
*/
|
|
ssize_t Curl_gtls_recv(struct connectdata *conn, /* connection data */
|
|
int num, /* socketindex */
|
|
char *buf, /* store read data here */
|
|
size_t buffersize, /* max amount to read */
|
|
bool *wouldblock)
|
|
{
|
|
ssize_t ret;
|
|
|
|
ret = gnutls_record_recv(conn->ssl[num].session, buf, buffersize);
|
|
if((ret == GNUTLS_E_AGAIN) || (ret == GNUTLS_E_INTERRUPTED)) {
|
|
*wouldblock = TRUE;
|
|
return -1;
|
|
}
|
|
|
|
if(ret == GNUTLS_E_REHANDSHAKE) {
|
|
/* BLOCKING call, this is bad but a work-around for now. Fixing this "the
|
|
proper way" takes a whole lot of work. */
|
|
CURLcode rc = handshake(conn, conn->ssl[num].session, num, FALSE);
|
|
if(rc)
|
|
/* handshake() writes error message on its own */
|
|
return rc;
|
|
*wouldblock = TRUE; /* then return as if this was a wouldblock */
|
|
return -1;
|
|
}
|
|
|
|
*wouldblock = FALSE;
|
|
if (!ret) {
|
|
failf(conn->data, "Peer closed the TLS connection");
|
|
return -1;
|
|
}
|
|
|
|
if (ret < 0) {
|
|
failf(conn->data, "GnuTLS recv error (%d): %s",
|
|
(int)ret, gnutls_strerror(ret));
|
|
return -1;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void Curl_gtls_session_free(void *ptr)
|
|
{
|
|
free(ptr);
|
|
}
|
|
|
|
size_t Curl_gtls_version(char *buffer, size_t size)
|
|
{
|
|
return snprintf(buffer, size, " GnuTLS/%s", gnutls_check_version(NULL));
|
|
}
|
|
|
|
#endif /* USE_GNUTLS */
|