Files
taskwarrior-2.x/src/TLSClient.cpp
Tomas Babej 282c59a394 TLSClient: Improve diagnostics
Task now correctly distinguishes the situation where CA file is present,
but not valid in some sense (empty file, not valid PEM, ..). In this
case the gnutls_certificate_set_x509_trust_file returns 0, as the number
of certificates detected in the file.

The method returns negative numbers for other errors, such as the CA
file itself missing.

Also clarify that when validating client cert/key pair, each of them can
be the source of the problem, not only the cliet certificate file.
2017-02-18 20:43:21 +01:00

583 lines
18 KiB
C++

////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// http://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#include <cmake.h>
#ifdef HAVE_LIBGNUTLS
#include <TLSClient.h>
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#if (defined OPENBSD || defined SOLARIS || defined NETBSD)
#include <errno.h>
#else
#include <sys/errno.h>
#endif
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <gnutls/x509.h>
#include <shared.h>
#include <format.h>
#define MAX_BUF 16384
#if GNUTLS_VERSION_NUMBER < 0x030406
#if GNUTLS_VERSION_NUMBER >= 0x020a00
static int verify_certificate_callback (gnutls_session_t);
#endif
#endif
////////////////////////////////////////////////////////////////////////////////
static void gnutls_log_function (int level, const char* message)
{
std::cout << "c: " << level << ' ' << message;
}
////////////////////////////////////////////////////////////////////////////////
#if GNUTLS_VERSION_NUMBER < 0x030406
#if GNUTLS_VERSION_NUMBER >= 0x020a00
static int verify_certificate_callback (gnutls_session_t session)
{
const TLSClient* client = (TLSClient*) gnutls_session_get_ptr (session); // All
return client->verify_certificate ();
}
#endif
#endif
////////////////////////////////////////////////////////////////////////////////
TLSClient::~TLSClient ()
{
gnutls_deinit (_session); // All
gnutls_certificate_free_credentials (_credentials); // All
#if GNUTLS_VERSION_NUMBER < 0x030300
gnutls_global_deinit (); // All
#endif
if (_socket)
{
shutdown (_socket, SHUT_RDWR);
close (_socket);
}
}
////////////////////////////////////////////////////////////////////////////////
void TLSClient::limit (int max)
{
_limit = max;
}
////////////////////////////////////////////////////////////////////////////////
// Calling this method results in all subsequent socket traffic being sent to
// std::cout, labelled with 'c: ...'.
void TLSClient::debug (int level)
{
if (level)
_debug = true;
gnutls_global_set_log_function (gnutls_log_function); // All
gnutls_global_set_log_level (level); // All
}
////////////////////////////////////////////////////////////////////////////////
void TLSClient::trust (const enum trust_level value)
{
_trust = value;
if (_debug)
{
if (_trust == allow_all)
std::cout << "c: INFO Server certificate will be trusted automatically.\n";
else if (_trust == ignore_hostname)
std::cout << "c: INFO Server certificate will be verified but hostname ignored.\n";
else
std::cout << "c: INFO Server certificate will be verified.\n";
}
}
////////////////////////////////////////////////////////////////////////////////
void TLSClient::ciphers (const std::string& cipher_list)
{
_ciphers = cipher_list;
}
////////////////////////////////////////////////////////////////////////////////
void TLSClient::init (
const std::string& ca,
const std::string& cert,
const std::string& key)
{
_ca = ca;
_cert = cert;
_key = key;
int ret;
#if GNUTLS_VERSION_NUMBER < 0x030300
ret = gnutls_global_init (); // All
if (ret < 0)
throw format ("TLS init error. {1}", gnutls_strerror (ret)); // All
#endif
ret = gnutls_certificate_allocate_credentials (&_credentials); // All
if (ret < 0)
throw format ("TLS allocation error. {1}", gnutls_strerror (ret)); // All
#if GNUTLS_VERSION_NUMBER >= 0x030014
// Automatic loading of system installed CA certificates.
ret = gnutls_certificate_set_x509_system_trust (_credentials); // 3.0.20
if (ret < 0)
throw format ("Bad System Trust. {1}", gnutls_strerror (ret)); // All
#endif
if (_ca != "")
{
// The gnutls_certificate_set_x509_key_file call returns number of
// certificates parsed on success (including 0, when no certificate was
// found) and negative values on error
ret = gnutls_certificate_set_x509_trust_file (_credentials, _ca.c_str (), GNUTLS_X509_FMT_PEM); // All
if (ret == 0)
throw format ("CA file {1} contains no certificate.", _ca);
else if (ret < 0)
throw format ("Bad CA file: {1}", gnutls_strerror (ret)); // All
}
// TODO This may need 0x030111 protection.
if (_cert != "" &&
_key != "" &&
(ret = gnutls_certificate_set_x509_key_file (_credentials, _cert.c_str (), _key.c_str (), GNUTLS_X509_FMT_PEM)) < 0) // 3.1.11
throw format ("Bad client CERT/KEY file. {1}", gnutls_strerror (ret)); // All
#if GNUTLS_VERSION_NUMBER < 0x030406
#if GNUTLS_VERSION_NUMBER >= 0x020a00
// The automatic verification for the server certificate with
// gnutls_certificate_set_verify_function only works with gnutls
// >=2.9.10. So with older versions we should call the verify function
// manually after the gnutls handshake.
gnutls_certificate_set_verify_function (_credentials, verify_certificate_callback); // 2.10.0
#endif
#endif
ret = gnutls_init (&_session, GNUTLS_CLIENT); // All
if (ret < 0)
throw format ("TLS client init error. {1}", gnutls_strerror (ret)); // All
// Use default priorities unless overridden.
if (_ciphers == "")
_ciphers = "NORMAL";
const char *err;
ret = gnutls_priority_set_direct (_session, _ciphers.c_str (), &err); // All
if (ret < 0)
{
if (_debug && ret == GNUTLS_E_INVALID_REQUEST)
std::cout << "c: ERROR Priority error at: " << err << '\n';
throw format ("Error initializing TLS. {1}", gnutls_strerror (ret)); // All
}
// Apply the x509 credentials to the current session.
ret = gnutls_credentials_set (_session, GNUTLS_CRD_CERTIFICATE, _credentials); // All
if (ret < 0)
throw format ("TLS credentials error. {1}", gnutls_strerror (ret)); // All
}
////////////////////////////////////////////////////////////////////////////////
void TLSClient::connect (const std::string& host, const std::string& port)
{
_host = host;
_port = port;
int ret;
#if GNUTLS_VERSION_NUMBER >= 0x030406
// For _trust == TLSClient::allow_all we perform no action
if (_trust == TLSClient::ignore_hostname)
gnutls_session_set_verify_cert (_session, NULL, 0); // 3.4.6
else if (_trust == TLSClient::strict)
gnutls_session_set_verify_cert (_session, _host.c_str (), 0); // 3.4.6
#endif
// SNI. Only permitted when _host is a DNS name, not an IPv4/6 address.
std::string dummyAddress;
int dummyPort;
if (! isIPv4Address (_host, dummyAddress, dummyPort) &&
! isIPv6Address (_host, dummyAddress, dummyPort))
{
ret = gnutls_server_name_set (_session, GNUTLS_NAME_DNS, _host.c_str (), _host.length ()); // All
if (ret < 0)
throw format ("TLS SNI error. {1}", gnutls_strerror (ret)); // All
}
// Store the TLSClient instance, so that the verification callback can access
// it during the handshake below and call the verification method.
gnutls_session_set_ptr (_session, (void*) this); // All
// use IPv4 or IPv6, does not matter.
struct addrinfo hints {};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // use my IP
struct addrinfo* res;
ret = ::getaddrinfo (host.c_str (), port.c_str (), &hints, &res);
if (ret != 0)
throw std::string (::gai_strerror (ret));
// Try them all, stop on success.
struct addrinfo* p;
for (p = res; p != NULL; p = p->ai_next)
{
if ((_socket = ::socket (p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
continue;
// When a socket is closed, it remains unavailable for a while (netstat -an).
// Setting SO_REUSEADDR allows this program to assume control of a closed,
// but unavailable socket.
int on = 1;
if (::setsockopt (_socket,
SOL_SOCKET,
SO_REUSEADDR,
(const void*) &on,
sizeof (on)) == -1)
throw std::string (::strerror (errno));
if (::connect (_socket, p->ai_addr, p->ai_addrlen) == -1)
continue;
break;
}
free (res);
if (p == NULL)
throw format ("Could not connect to {1} {2}", host, port);
#if GNUTLS_VERSION_NUMBER >= 0x030100
gnutls_handshake_set_timeout (_session, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT); // 3.1.0
#endif
#if GNUTLS_VERSION_NUMBER >= 0x030109
gnutls_transport_set_int (_session, _socket); // 3.1.9
#else
gnutls_transport_set_ptr (_session, (gnutls_transport_ptr_t) (intptr_t) _socket); // All
#endif
// Perform the TLS handshake
do
{
ret = gnutls_handshake (_session); // All
}
while (ret < 0 && gnutls_error_is_fatal (ret) == 0); // All
if (ret < 0)
{
#if GNUTLS_VERSION_NUMBER >= 0x030406
if (ret == GNUTLS_E_CERTIFICATE_VERIFICATION_ERROR)
{
auto type = gnutls_certificate_type_get (_session); // All
auto status = gnutls_session_get_verify_cert_status (_session); // 3.4.6
gnutls_datum_t out;
gnutls_certificate_verification_status_print (status, type, &out, 0); // 3.1.4
std::string error {(const char*) out.data};
gnutls_free (out.data); // All
throw format ("Handshake failed. {1}", error); // All
}
#else
throw format ("Handshake failed. {1}", gnutls_strerror (ret)); // All
#endif
}
#if GNUTLS_VERSION_NUMBER < 0x020a00
// The automatic verification for the server certificate with
// gnutls_certificate_set_verify_function does only work with gnutls
// >=2.10.0. So with older versions we should call the verify function
// manually after the gnutls handshake.
ret = verify_certificate ();
if (ret < 0)
{
if (_debug)
std::cout << "c: ERROR Certificate verification failed.\n";
throw format ("Error initializing TLS. {1}", gnutls_strerror (ret)); // All
}
#endif
if (_debug)
{
#if GNUTLS_VERSION_NUMBER >= 0x03010a
char* desc = gnutls_session_get_desc (_session); // 3.1.10
std::cout << "c: INFO Handshake was completed: " << desc << '\n';
gnutls_free (desc);
#else
std::cout << "c: INFO Handshake was completed.\n";
#endif
}
}
////////////////////////////////////////////////////////////////////////////////
void TLSClient::bye ()
{
gnutls_bye (_session, GNUTLS_SHUT_RDWR); // All
}
////////////////////////////////////////////////////////////////////////////////
int TLSClient::verify_certificate () const
{
if (_trust == TLSClient::allow_all)
return 0;
if (_debug)
std::cout << "c: INFO Verifying certificate.\n";
// This verification function uses the trusted CAs in the credentials
// structure. So you must have installed one or more CA certificates.
unsigned int status = 0;
const char* hostname = _host.c_str();
#if GNUTLS_VERSION_NUMBER >= 0x030104
if (_trust == TLSClient::ignore_hostname)
hostname = NULL;
int ret = gnutls_certificate_verify_peers3 (_session, hostname, &status); // 3.1.4
if (ret < 0)
{
if (_debug)
std::cout << "c: ERROR Certificate verification peers3 failed. " << gnutls_strerror (ret) << '\n'; // All
return GNUTLS_E_CERTIFICATE_ERROR;
}
// status 16450 == 0100000001000010
// GNUTLS_CERT_INVALID 1<<1
// GNUTLS_CERT_SIGNER_NOT_FOUND 1<<6
// GNUTLS_CERT_UNEXPECTED_OWNER 1<<14 Hostname does not match
if (_debug && status)
std::cout << "c: ERROR Certificate status=" << status << '\n';
#else
int ret = gnutls_certificate_verify_peers2 (_session, &status); // All
if (ret < 0)
{
if (_debug)
std::cout << "c: ERROR Certificate verification peers2 failed. " << gnutls_strerror (ret) << '\n'; // All
return GNUTLS_E_CERTIFICATE_ERROR;
}
if (_debug && status)
std::cout << "c: ERROR Certificate status=" << status << '\n';
if ((status == 0) && (_trust != TLSClient::ignore_hostname))
{
if (gnutls_certificate_type_get (_session) == GNUTLS_CRT_X509) // All
{
const gnutls_datum* cert_list;
unsigned int cert_list_size;
gnutls_x509_crt cert;
cert_list = gnutls_certificate_get_peers (_session, &cert_list_size); // All
if (cert_list_size == 0)
{
if (_debug)
std::cout << "c: ERROR Certificate get peers failed. " << gnutls_strerror (ret) << '\n'; // All
return GNUTLS_E_CERTIFICATE_ERROR;
}
ret = gnutls_x509_crt_init (&cert); // All
if (ret < 0)
{
if (_debug)
std::cout << "c: ERROR x509 init failed. " << gnutls_strerror (ret) << '\n'; // All
return GNUTLS_E_CERTIFICATE_ERROR;
}
ret = gnutls_x509_crt_import (cert, &cert_list[0], GNUTLS_X509_FMT_DER); // All
if (ret < 0)
{
if (_debug)
std::cout << "c: ERROR x509 cert import. " << gnutls_strerror (ret) << '\n'; // All
gnutls_x509_crt_deinit(cert); // All
return GNUTLS_E_CERTIFICATE_ERROR;
}
if (gnutls_x509_crt_check_hostname (cert, hostname) == 0) // All
{
if (_debug)
std::cout << "c: ERROR x509 cert check hostname. " << gnutls_strerror (ret) << '\n'; // All
gnutls_x509_crt_deinit(cert);
return GNUTLS_E_CERTIFICATE_ERROR;
}
}
else
return GNUTLS_E_CERTIFICATE_ERROR;
}
#endif
#if GNUTLS_VERSION_NUMBER >= 0x030104
gnutls_certificate_type_t type = gnutls_certificate_type_get (_session); // All
gnutls_datum_t out;
ret = gnutls_certificate_verification_status_print (status, type, &out, 0); // 3.1.4
if (ret < 0)
{
if (_debug)
std::cout << "c: ERROR certificate verification status. " << gnutls_strerror (ret) << '\n'; // All
return GNUTLS_E_CERTIFICATE_ERROR;
}
if (_debug)
std::cout << "c: INFO " << out.data << '\n';
gnutls_free (out.data);
#endif
if (status != 0)
return GNUTLS_E_CERTIFICATE_ERROR;
// Continue handshake.
return 0;
}
////////////////////////////////////////////////////////////////////////////////
void TLSClient::send (const std::string& data)
{
std::string packet = "XXXX" + data;
// Encode the length.
unsigned long l = packet.length ();
packet[0] = l >>24;
packet[1] = l >>16;
packet[2] = l >>8;
packet[3] = l;
unsigned int total = 0;
unsigned int remaining = packet.length ();
while (total < packet.length ())
{
int status;
do
{
status = gnutls_record_send (_session, packet.c_str () + total, remaining); // All
}
while (errno == GNUTLS_E_INTERRUPTED ||
errno == GNUTLS_E_AGAIN);
if (status == -1)
break;
total += (unsigned int) status;
remaining -= (unsigned int) status;
}
if (_debug)
std::cout << "c: INFO Sending 'XXXX"
<< data.c_str ()
<< "' (" << total << " bytes)"
<< std::endl;
}
////////////////////////////////////////////////////////////////////////////////
void TLSClient::recv (std::string& data)
{
data = ""; // No appending of data.
int received = 0;
// Get the encoded length.
unsigned char header[4] {};
do
{
received = gnutls_record_recv (_session, header, 4); // All
}
while (received > 0 &&
(errno == GNUTLS_E_INTERRUPTED ||
errno == GNUTLS_E_AGAIN));
int total = received;
// Decode the length.
unsigned long expected = (header[0]<<24) |
(header[1]<<16) |
(header[2]<<8) |
header[3];
if (_debug)
std::cout << "c: INFO expecting " << expected << " bytes.\n";
// TODO This would be a good place to assert 'expected < _limit'.
// Arbitrary buffer size.
char buffer[MAX_BUF];
// Keep reading until no more data. Concatenate chunks of data if a) the
// read was interrupted by a signal, and b) if there is more data than
// fits in the buffer.
do
{
do
{
received = gnutls_record_recv (_session, buffer, MAX_BUF - 1); // All
}
while (received > 0 &&
(errno == GNUTLS_E_INTERRUPTED ||
errno == GNUTLS_E_AGAIN));
// Other end closed the connection.
if (received == 0)
{
if (_debug)
std::cout << "c: INFO Peer has closed the TLS connection\n";
break;
}
// Something happened.
if (received < 0 && gnutls_error_is_fatal (received) == 0) // All
{
if (_debug)
std::cout << "c: WARNING " << gnutls_strerror (received) << '\n'; // All
}
else if (received < 0)
throw std::string (gnutls_strerror (received)); // All
buffer [received] = '\0';
data += buffer;
total += received;
// Stop at defined limit.
if (_limit && total > _limit)
break;
}
while (received > 0 && total < (int) expected);
if (_debug)
std::cout << "c: INFO Receiving 'XXXX"
<< data.c_str ()
<< "' (" << total << " bytes)"
<< std::endl;
}
////////////////////////////////////////////////////////////////////////////////
#endif