File: tlsstream.cpp

package info (click to toggle)
icinga2 2.15.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 20,040 kB
  • sloc: cpp: 97,870; sql: 3,261; cs: 1,636; yacc: 1,584; sh: 1,009; ansic: 890; lex: 420; python: 80; makefile: 62; javascript: 12
file content (166 lines) | stat: -rw-r--r-- 5,133 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */

#include "base/tlsstream.hpp"
#include "base/application.hpp"
#include "base/utility.hpp"
#include "base/exception.hpp"
#include "base/logger.hpp"
#include "base/configuration.hpp"
#include "base/convert.hpp"
#include "base/defer.hpp"
#include "base/io-engine.hpp"
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/verify_context.hpp>
#include <boost/asio/ssl/verify_mode.hpp>
#include <iostream>
#include <openssl/ssl.h>
#include <openssl/tls1.h>
#include <openssl/x509.h>
#include <sstream>

using namespace icinga;

/**
 * Checks whether the TLS handshake was completed with a valid peer certificate.
 *
 * @return true if the peer presented a valid certificate, false otherwise
 */
bool UnbufferedAsioTlsStream::IsVerifyOK()
{
	if (!SSL_is_init_finished(native_handle())) {
		// handshake was not completed
		return false;
	}

	if (GetPeerCertificate() == nullptr) {
		// no peer certificate was sent
		return false;
	}

	return SSL_get_verify_result(native_handle()) == X509_V_OK;
}

/**
 * Returns a human-readable error string for situations where IsVerifyOK() returns false.
 *
 * If the handshake was completed and a peer certificate was provided,
 * the string additionally contains the OpenSSL verification error code.
 *
 * @return string containing the error message
 */
String UnbufferedAsioTlsStream::GetVerifyError()
{
	if (!SSL_is_init_finished(native_handle())) {
		return "handshake not completed";
	}

	if (GetPeerCertificate() == nullptr) {
		return "no peer certificate provided";
	}

	std::ostringstream buf;
	long err = SSL_get_verify_result(native_handle());
	buf << "code " << err << ": " << X509_verify_cert_error_string(err);
	return buf.str();
}

std::shared_ptr<X509> UnbufferedAsioTlsStream::GetPeerCertificate()
{
	return std::shared_ptr<X509>(SSL_get_peer_certificate(native_handle()), X509_free);
}

void UnbufferedAsioTlsStream::BeforeHandshake(handshake_type type)
{
	namespace ssl = boost::asio::ssl;

	if (!m_Hostname.IsEmpty()) {
		X509_VERIFY_PARAM_set1_host(SSL_get0_param(native_handle()), m_Hostname.CStr(), m_Hostname.GetLength());
	}

	set_verify_mode(ssl::verify_peer | ssl::verify_client_once);

	set_verify_callback([](bool preverified, ssl::verify_context& ctx) {
		(void) preverified;
		(void) ctx;

		/* Continue the handshake even if an invalid peer certificate was presented. The verification result has to be
		 * checked using the IsVerifyOK() method.
		 *
		 * Such connections are used for the initial enrollment of nodes where they use a self-signed certificate to
		 * send a certificate request and receive their valid certificate after approval (manually by the administrator
		 * or using a certificate ticket).
		 */
		return true;
	});

#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
	if (type == client && !m_Hostname.IsEmpty()) {
		String environmentName = Application::GetAppEnvironment();
		String serverName = m_Hostname;

		if (!environmentName.IsEmpty())
			serverName += ":" + environmentName;

		SSL_set_tlsext_host_name(native_handle(), serverName.CStr());
	}
#endif /* SSL_CTRL_SET_TLSEXT_HOSTNAME */
}

/**
 * Forcefully close the connection, typically (details are up to the operating system) using a TCP RST.
 */
void AsioTlsStream::ForceDisconnect()
{
	if (!lowest_layer().is_open()) {
		// Already disconnected, nothing to do.
		return;
	}

	boost::system::error_code ec;

	// Close the socket. In case the connection wasn't shut down cleanly by GracefulDisconnect(), the operating system
	// will typically terminate the connection with a TCP RST. Otherwise, this just releases the file descriptor.
	lowest_layer().close(ec);
}

/**
 * Try to cleanly shut down the connection. This involves sending a TLS close_notify shutdown alert and terminating the
 * underlying TCP connection. Sending these additional messages can block, hence the method takes a yield context and
 * internally implements a timeout of 10 seconds for the operation after which the connection is forcefully terminated
 * using ForceDisconnect().
 *
 * @param strand Asio strand used for other operations on this connection.
 * @param yc Yield context for Asio coroutines
 */
void AsioTlsStream::GracefulDisconnect(boost::asio::io_context::strand& strand, boost::asio::yield_context& yc)
{
	if (!lowest_layer().is_open()) {
		// Already disconnected, nothing to do.
		return;
	}

	{
		Timeout shutdownTimeout (strand, boost::posix_time::seconds(10),
			[this] {
				// Forcefully terminate the connection if async_shutdown() blocked more than 10 seconds.
				ForceDisconnect();
			}
		);

		// Close the TLS connection, effectively uses SSL_shutdown() to send a close_notify shutdown alert to the peer.
		boost::system::error_code ec;
		next_layer().async_shutdown(yc[ec]);
	}

	if (!lowest_layer().is_open()) {
		// Connection got closed in the meantime, most likely by the timeout, so nothing more to do.
		return;
	}

	// Shut down the TCP connection.
	boost::system::error_code ec;
	lowest_layer().shutdown(lowest_layer_type::shutdown_both, ec);

	// Clean up the connection (closes the file descriptor).
	ForceDisconnect();
}