Files
wget2/libwget/ssl_openssl.c
DalmeGNU 9395dd4b79 Implement --check-certificate=quiet
* include/wget/wget.h: Add WGET_SSL_REPORT_INVALID_CERT macro
* libwget/ssl_gnutls.c: Change certificate related error_printf() calls, for error_printf_check()
* libwget/ssl_openssl.c: Consider the case WGET_SSL_REPORT_INVALID_CERT in wget_ssl_set_config_int()
* libwget/ssl_wolfssl.c: Change certificate related error_printf() calls, for error_printf_check()
* src/options.c: Admit quiet as a valid value on --check-certificate
* src/wget_options.h: Move check_certificate from bool to an enum type (with enabled, disabled and log_disabled as values)
2022-07-23 16:28:22 +02:00

1831 lines
52 KiB
C

/*
* Copyright (c) 2015-2022 Free Software Foundation, Inc.
*
* This file is part of libwget.
*
* Libwget is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Libwget 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with libwget. If not, see <https://www.gnu.org/licenses/>.
*
*
* SSL/TLS routines, with OpenSSL as the backend engine
*
* Author: Ander Juaristi
*/
#include <config.h>
#include <dirent.h>
#include <limits.h> // INT_MAX
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/ocsp.h>
#include <openssl/crypto.h>
#include <openssl/ossl_typ.h>
#include <openssl/x509.h>
#include <openssl/x509_vfy.h>
#include <openssl/x509v3.h>
#include <openssl/asn1.h>
#ifdef _WIN32
# include <w32sock.h>
#else
# define FD_TO_SOCKET(x) (x)
# define SOCKET_TO_FD(x) (x)
#endif
#ifdef LIBRESSL_VERSION_NUMBER
#ifndef TLS_MAX_VERSION
#ifndef TLS1_3_VERSION
#define TLS1_3_VERSION TLS1_2_VERSION
#define TLS_MAX_VERSION TLS1_2_VERSION
#else
#define TLS_MAX_VERSION TLS1_3_VERSION
#endif
#endif
#endif
#include <wget.h>
#include "private.h"
#include "net.h"
static wget_tls_stats_callback
*tls_stats_callback;
static void
*tls_stats_ctx;
static wget_ocsp_stats_callback
*ocsp_stats_callback;
static void
*ocsp_stats_ctx;
static struct config
{
const char
*secure_protocol,
*ca_directory,
*ca_file,
*cert_file,
*key_file,
*crl_file,
*ocsp_server,
*alpn;
wget_ocsp_db
*ocsp_cert_cache,
*ocsp_host_cache;
wget_tls_session_db
*tls_session_cache;
wget_hpkp_db
*hpkp_cache;
char
ca_type,
cert_type,
key_type;
bool
check_certificate :1,
check_hostname :1,
print_info :1,
ocsp :1,
ocsp_date :1,
ocsp_stapling :1,
ocsp_nonce :1;
} config = {
.check_certificate = 1,
.check_hostname = 1,
#ifdef WITH_OCSP
.ocsp = 1,
.ocsp_stapling = 1,
#endif
.ca_type = WGET_SSL_X509_FMT_PEM,
.cert_type = WGET_SSL_X509_FMT_PEM,
.key_type = WGET_SSL_X509_FMT_PEM,
.secure_protocol = "AUTO",
.ca_directory = "system",
#ifdef WITH_LIBNGHTTP2
.alpn = "h2,http/1.1"
#endif
};
static int init;
static wget_thread_mutex mutex;
static SSL_CTX *_ctx;
static int store_userdata_idx;
/*
* Constructor & destructor
*/
static void __attribute__ ((constructor)) tls_init(void)
{
if (!mutex)
wget_thread_mutex_init(&mutex);
store_userdata_idx = X509_STORE_CTX_get_ex_new_index(
0, NULL, /* argl, argp */
NULL, /* new_func */
NULL, /* dup_func */
NULL); /* free_func */
}
static void __attribute__ ((destructor)) tls_exit(void)
{
if (mutex)
wget_thread_mutex_destroy(&mutex);
}
/*
* SSL/TLS configuration functions
*/
/**
* \param[in] key An identifier for the config parameter (starting with `WGET_SSL_`) to set
* \param[in] value The value for the config parameter (a NULL-terminated string)
*
* Set a configuration parameter, as a string.
*
* The following parameters accept a string as their value (\p key can have any of those values):
*
* - WGET_SSL_SECURE_PROTOCOL: A string describing which SSL/TLS version should be used. It can have either
* an arbitrary value, or one of the following fixed values (case does not matter):
* - "SSL": SSLv3 will be used. Warning: this protocol is insecure and should be avoided.
* - "TLSv1": TLS 1.0 will be used.
* - "TLSv1_1": TLS 1.1 will be used.
* - "TLSv1_2": TLS 1.2 will be used.
* - "TLSv1_3": TLS 1.3 will be used.
* - "AUTO": Let the TLS library decide.
* - "PFS": Let the TLS library decide, but make sure only forward-secret ciphers are used.
*
* An arbitrary string can also be supplied (an string that's different from any of the previous ones). If that's the case
* the string will be directly taken as the priority string and sent to the library. Priority strings provide the greatest flexibility,
* but have a library-specific syntax. A GnuTLS priority string will not work if your libwget has been compiled with OpenSSL, for instance.
* - WGET_SSL_CA_DIRECTORY: A path to the directory where the root certificates will be taken from
* for server cert validation. Every file of that directory is expected to contain an X.509 certificate,
* encoded in PEM format. If the string "system" is specified, the system's default directory will be used.
* The default value is "system". Certificates get loaded in wget_ssl_init().
* - WGET_SSL_CA_FILE: A path to a file containing a single root certificate. This will be used to validate
* the server's certificate chain. This option can be used together with `WGET_SSL_CA_DIRECTORY`. The certificate
* can be in either PEM or DER format. The format is specified in the `WGET_SSL_CA_TYPE` option (see
* wget_ssl_set_config_int()).
* - WGET_SSL_CERT_FILE: Set the client certificate. It will be used for client authentication if the server requests it.
* It can be in either PEM or DER format. The format is specified in the `WGET_SSL_CERT_TYPE` option (see
* wget_ssl_set_config_int()). The `WGET_SSL_KEY_FILE` option specifies the private key corresponding to the cert's
* public key. If `WGET_SSL_KEY_FILE` is not set, then the private key is expected to be in the same file as the certificate.
* - WGET_SSL_KEY_FILE: Set the private key corresponding to the client certificate specified in `WGET_SSL_CERT_FILE`.
* It can be in either PEM or DER format. The format is specified in the `WGET_SSL_KEY_TYPE` option (see
* wget_ssl_set_config_int()). IF `WGET_SSL_CERT_FILE` is not set, then the certificate is expected to be in the same file
* as the private key.
* - WGET_SSL_CRL_FILE: Sets a CRL (Certificate Revocation List) file which will be used to verify client and server certificates.
* A CRL file is a black list that contains the serial numbers of the certificates that should not be treated as valid. Whenever
* a client or a server presents a certificate in the TLS handshake whose serial number is contained in the CRL, the handshake
* will be immediately aborted. The CRL file must be in PEM format.
* - WGET_SSL_OCSP_SERVER: Set the URL of the OCSP server that will be used to validate certificates.
* OCSP is a protocol by which a server is queried to tell whether a given certificate is valid or not. It's an approach contrary
* to that used by CRLs. While CRLs are black lists, OCSP takes a white list approach where a certificate can be checked for validity.
* Whenever a client or server presents a certificate in a TLS handshake, the provided URL will be queried (using OCSP) to check whether
* that certificate is valid or not. If the server responds the certificate is not valid, the handshake will be immediately aborted.
* - WGET_SSL_ALPN: Sets the ALPN string to be sent to the remote host. ALPN is a TLS extension
* ([RFC 7301](https://tools.ietf.org/html/rfc7301))
* that allows both the server and the client to signal which application-layer protocols they support (HTTP/2, QUIC, etc.).
* That information can then be used for the server to ultimately decide which protocol will be used on top of TLS.
*
* An invalid value for \p key will not harm the operation of TLS, but will cause
* a complain message to be printed to the error log stream.
*/
void wget_ssl_set_config_string(int key, const char *value)
{
switch (key) {
case WGET_SSL_SECURE_PROTOCOL:
config.secure_protocol = value;
break;
case WGET_SSL_CA_DIRECTORY:
config.ca_directory = value;
break;
case WGET_SSL_CA_FILE:
config.ca_file = value;
break;
case WGET_SSL_CERT_FILE:
config.cert_file = value;
break;
case WGET_SSL_KEY_FILE:
config.key_file = value;
break;
case WGET_SSL_CRL_FILE:
config.crl_file = value;
break;
case WGET_SSL_OCSP_SERVER:
config.ocsp_server = value;
break;
case WGET_SSL_ALPN:
config.alpn = value;
break;
default:
error_printf(_("Unknown configuration key %d (maybe this config value should be of another type?)\n"), key);
}
}
/**
* \param[in] key An identifier for the config parameter (starting with `WGET_SSL_`) to set
* \param[in] value The value for the config parameter (a pointer)
*
* Set a configuration parameter, as a libwget object.
*
* The following parameters expect an already initialized libwget object as their value.
*
* - WGET_SSL_OCSP_CACHE: This option takes a pointer to a \ref wget_ocsp_db
* structure as an argument. Such a pointer is returned when initializing the OCSP cache with wget_ocsp_db_init().
* The cache is used to store OCSP responses locally and avoid querying the OCSP server repeatedly for the same certificate.
* - WGET_SSL_SESSION_CACHE: This option takes a pointer to a \ref wget_tls_session_db structure.
* Such a pointer is returned when initializing the TLS session cache with wget_tls_session_db_init().
* This option thus sets the handle to the TLS session cache that will be used to store TLS sessions.
* The TLS session cache is used to support TLS session resumption. It stores the TLS session parameters derived from a previous TLS handshake
* (most importantly the session identifier and the master secret) so that there's no need to run the handshake again
* the next time we connect to the same host. This is useful as the handshake is an expensive process.
* - WGET_SSL_HPKP_CACHE: Set the HPKP cache to be used to verify known HPKP pinned hosts. This option takes a pointer
* to a \ref wget_hpkp_db structure. Such a pointer is returned when initializing the HPKP cache
* with wget_hpkp_db_init(). HPKP is a HTTP-level protocol that allows the server to "pin" its present and future X.509
* certificate fingerprints, to support rapid certificate change in the event that the higher level root CA
* gets compromised ([RFC 7469](https://tools.ietf.org/html/rfc7469)).
*/
void wget_ssl_set_config_object(int key, void *value)
{
switch (key) {
case WGET_SSL_OCSP_CACHE:
config.ocsp_cert_cache = (wget_ocsp_db *) value;
break;
case WGET_SSL_SESSION_CACHE:
config.tls_session_cache = (wget_tls_session_db *) value;
break;
case WGET_SSL_HPKP_CACHE:
config.hpkp_cache = (wget_hpkp_db *) value;
break;
default:
error_printf(_("Unknown configuration key %d (maybe this config value should be of another type?)\n"), key);
}
}
/**
* \param[in] key An identifier for the config parameter (starting with `WGET_SSL_`)
* \param[in] value The value for the config parameter
*
* Set a configuration parameter, as an integer.
*
* These are the parameters that can be set (\p key can have any of these values):
*
* - WGET_SSL_CHECK_CERTIFICATE: whether certificates should be verified (1) or not (0)
* - WGET_SSL_REPORT_INVALID_CERT: currently ignored on the OpenSSL backend
* - WGET_SSL_CHECK_HOSTNAME: whether or not to check if the certificate's subject field
* matches the peer's hostname. This check is done according to the rules in [RFC 6125](https://tools.ietf.org/html/rfc6125)
* and typically involves checking whether the hostname and the common name (CN) field of the subject match.
* - WGET_SSL_PRINT_INFO: whether or not information should be printed about the established SSL/TLS handshake (negotiated
* ciphersuites, certificates, etc.). The default is no (0).
*
* The following three options all can take either `WGET_SSL_X509_FMT_PEM` (to specify the PEM format) or `WGET_SSL_X509_FMT_DER`
* (for the DER format). The default in for all of them is `WGET_SSL_X509_FMT_PEM`.
*
* - WGET_SSL_CA_TYPE: Specifies what's the format of the root CA certificate(s) supplied with either `WGET_SSL_CA_DIRECTORY`
* or `WGET_SSL_CA_FILE`.
* - WGET_SSL_CERT_TYPE: Specifies what's the format of the certificate file supplied with `WGET_SSL_CERT_FILE`. **The certificate
* and the private key supplied must both be of the same format.**
* - WGET_SSL_KEY_TYPE: Specifies what's the format of the private key file supplied with `WGET_SSL_KEY_FILE`. **The private key
* and the certificate supplied must both be of the same format.**
*
* The following two options control OCSP queries. These don't affect the CRL set with `WGET_SSL_CRL_FILE`, if any.
* If both CRLs and OCSP are enabled, both will be used.
*
* - WGET_SSL_OCSP: whether or not OCSP should be used. The default is yes (1).
* - WGET_SSL_OCSP_STAPLING: whether or not OCSP stapling should be used. The default is yes (1).
* - WGET_SSL_OCSP_NONCE: whether or not an OCSP nonce should be sent in the request. The default is yes (1).
* If a nonce was sent in the request, the OCSP verification will fail if the response nonce doesn't match.
* However if the response does not include a nonce extension, verification will be allowed to continue.
* The OCSP nonce extension is not a critical one.
* - WGET_SSL_OCSP_DATE: Reject the OCSP response if it's older than 3 days.
*/
void wget_ssl_set_config_int(int key, int value)
{
switch (key) {
case WGET_SSL_CHECK_CERTIFICATE:
config.check_certificate = value;
break;
case WGET_SSL_REPORT_INVALID_CERT:
// The OpenSSL backend doesn't report any certificate errors if certificate verification is disabled
break;
case WGET_SSL_CHECK_HOSTNAME:
config.check_hostname = value;
break;
case WGET_SSL_PRINT_INFO:
config.print_info = value;
break;
case WGET_SSL_CA_TYPE:
config.ca_type = (char) value;
break;
case WGET_SSL_CERT_TYPE:
config.cert_type = (char) value;
break;
case WGET_SSL_KEY_TYPE:
config.key_type = (char) value;
break;
case WGET_SSL_OCSP:
config.ocsp = value;
break;
case WGET_SSL_OCSP_STAPLING:
config.ocsp_stapling = value;
break;
case WGET_SSL_OCSP_NONCE:
config.ocsp_nonce = value;
break;
case WGET_SSL_OCSP_DATE:
config.ocsp_date = value;
break;
default:
error_printf(_("Unknown configuration key %d (maybe this config value should be of another type?)\n"), key);
}
}
/*
* SSL/TLS core public API
*/
static int openssl_load_crl(X509_STORE *store, const char *crl_file)
{
X509_LOOKUP *lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file());
if (!X509_load_crl_file(lookup, crl_file, X509_FILETYPE_PEM))
return WGET_E_UNKNOWN;
if (!X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL | X509_V_FLAG_USE_DELTAS))
return WGET_E_UNKNOWN;
return 0;
}
#define SET_MIN_VERSION(ctx, ver) \
if (!SSL_CTX_set_min_proto_version(ctx, ver)) \
return WGET_E_UNKNOWN
static int openssl_set_priorities(SSL_CTX *ctx, const char *prio)
{
/*
* Default ciphers. This is what will be used
* if 'auto' is specified as the priority (currently the default).
*/
const char *openssl_ciphers = "HIGH:!aNULL:!RC4:!MD5:!SRP:!PSK";
SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
SSL_CTX_set_max_proto_version(ctx, TLS_MAX_VERSION);
if (!wget_strcasecmp_ascii(prio, "SSL")) {
SET_MIN_VERSION(ctx, SSL3_VERSION);
} else if (!wget_strcasecmp_ascii(prio, "TLSv1")) {
SET_MIN_VERSION(ctx, TLS1_VERSION);
} else if (!wget_strcasecmp_ascii(prio, "TLSv1_1")) {
SET_MIN_VERSION(ctx, TLS1_1_VERSION);
/*
* Skipping "TLSv1_2".
* Checking for "TLSv1_2" is totally redundant - we already set it as the minimum supported version by default
*/
} else if (!wget_strcasecmp_ascii(prio, "TLSv1_3")) {
/* OpenSSL supports TLS 1.3 starting at 1.1.1-beta9 (0x10101009) */
#if OPENSSL_VERSION_NUMBER >= 0x10101009
SET_MIN_VERSION(ctx, TLS1_3_VERSION);
#else
info_printf(_("OpenSSL: TLS 1.3 is not supported by your OpenSSL version. Will use TLS 1.2 instead.\n"));
#endif
} else if (!wget_strcasecmp_ascii(prio, "PFS")) {
/* Forward-secrecy - Disable RSA key exchange! */
openssl_ciphers = "HIGH:!aNULL:!RC4:!MD5:!SRP:!PSK:!kRSA";
} else if (prio && wget_strcasecmp_ascii(prio, "AUTO") && wget_strcasecmp_ascii(prio, "TLSv1_2")) {
openssl_ciphers = prio;
}
if (!SSL_CTX_set_cipher_list(ctx, openssl_ciphers)) {
error_printf(_("OpenSSL: Invalid priority string '%s'\n"), prio);
return WGET_E_INVALID;
}
return 0;
}
static int openssl_load_trust_file(SSL_CTX *ctx, const char *dir, const char *file)
{
char sbuf[256];
wget_buffer buf;
int rc;
wget_buffer_init(&buf, sbuf, sizeof(sbuf));
wget_buffer_printf(&buf, "%s/%s", dir, file);
rc = (SSL_CTX_load_verify_locations(ctx, buf.data, NULL) ? 0 : -1);
wget_buffer_deinit(&buf);
return rc;
}
static int openssl_load_trust_files_from_directory(SSL_CTX *ctx, const char *dirname)
{
DIR *dir;
struct dirent *dp;
int loaded = 0;
if ((dir = opendir(dirname))) {
while ((dp = readdir(dir))) {
if (*dp->d_name == '.')
continue;
if (wget_match_tail_nocase(dp->d_name, ".pem")
&& openssl_load_trust_file(ctx, dirname, dp->d_name) == 0)
{
loaded++;
}
}
closedir(dir);
}
return loaded;
}
static int openssl_load_trust_files(SSL_CTX *ctx, const char *dir)
{
int retval;
if (!strcmp(dir, "system")) {
/*
* Load system-provided certificates.
* Either "/etc/ssl/certs" or OpenSSL's default (if provided).
*/
if (SSL_CTX_set_default_verify_paths(ctx)) {
retval = 0;
goto end;
}
dir = "/etc/ssl/certs";
info_printf(_("OpenSSL: Could not load certificates from default paths. Falling back to '%s'."), dir);
}
retval = openssl_load_trust_files_from_directory(ctx, dir);
if (retval == 0)
error_printf(_("OpenSSL: No certificates could be loaded from directory '%s'\n"), dir);
else if (retval > 0)
debug_printf("OpenSSL: Loaded %d certificates\n", retval);
else
error_printf(_("OpenSSL: Could not open directory '%s'. No certificates were loaded.\n"), dir);
end:
return retval;
}
static int verify_hpkp(const char *hostname, X509 *subject_cert, wget_hpkp_stats_result *hpkp_stats)
{
int retval, spki_len;
unsigned char *spki = NULL;
/* Get certificate's public key in DER format */
spki_len = i2d_PUBKEY(X509_get0_pubkey(subject_cert), &spki);
if (spki_len <= 0)
return -1;
/* Lookup database */
retval = wget_hpkp_db_check_pubkey(config.hpkp_cache,
hostname,
spki, spki_len);
switch (retval) {
case 1:
debug_printf("Matching HPKP pinning found for host '%s'\n", hostname);
*hpkp_stats = WGET_STATS_HPKP_MATCH;
retval = 0;
break;
case 0:
debug_printf("No HPKP pinning found for host '%s'\n", hostname);
*hpkp_stats = WGET_STATS_HPKP_NO;
retval = 1;
break;
case -2:
debug_printf("Public key for host '%s' does not match\n", hostname);
*hpkp_stats = WGET_STATS_HPKP_NOMATCH;
retval = -1;
break;
default:
debug_printf("Could not check HPKP pinning for host '%s' (%d)\n", hostname, retval);
*hpkp_stats = WGET_STATS_HPKP_ERROR;
retval = 0;
}
OPENSSL_free(spki);
return retval;
}
static int check_cert_chain_for_hpkp(STACK_OF(X509) *certs, const char *hostname, wget_hpkp_stats_result *hpkp_stats)
{
int retval, pin_ok = 0;
X509 *cert;
unsigned cert_list_size = sk_X509_num(certs);
for (unsigned i = 0; i < cert_list_size; i++) {
cert = sk_X509_value(certs, i);
if ((retval = verify_hpkp(hostname, cert, hpkp_stats)) >= 0)
pin_ok = 1;
if (retval == 1)
break;
}
return pin_ok;
}
struct verification_flags {
SSL
*ssl;
const char
*hostname;
X509_STORE
*certstore;
unsigned int
cert_chain_size;
wget_hpkp_stats_result
hpkp_stats;
bool
verifying_ocsp,
ocsp_checked;
};
static int check_ocsp_response(OCSP_RESPONSE *,
STACK_OF(X509) *,
X509_STORE *,
bool);
static int ocsp_resp_cb(SSL *s, void *arg)
{
int result;
long ocsp_resp_len;
const unsigned char *ocsp_resp_raw = NULL;
OCSP_RESPONSE *ocspresp;
STACK_OF(X509) *certstack;
struct verification_flags *vflags = arg;
if (!vflags)
return 0;
ocsp_resp_len = SSL_get_tlsext_status_ocsp_resp(s, &ocsp_resp_raw);
if (ocsp_resp_len == -1) {
debug_printf("No stapled OCSP response was received. Continuing.\n");
return 1;
}
ocspresp = d2i_OCSP_RESPONSE(NULL, &ocsp_resp_raw, ocsp_resp_len);
if (!ocspresp) {
error_printf(_("Got a stapled OCSP response, but could not parse it. Aborting.\n"));
return 0;
}
certstack = SSL_get_peer_cert_chain(vflags->ssl);
if (!certstack) {
error_printf(_("Could not get server's cert stack\n"));
return 0;
}
result = check_ocsp_response(ocspresp,
certstack,
vflags->certstore,
0);
if (result == -1) {
OCSP_RESPONSE_free(ocspresp);
error_printf(_("Could not verify stapled OCSP response. Aborting.\n"));
return 0;
}
OCSP_RESPONSE_free(ocspresp);
debug_printf("Got a stapled OCSP response. Length: %ld. Status: OK\n", ocsp_resp_len);
return 1;
}
static OCSP_REQUEST *send_ocsp_request(const char *uri,
OCSP_CERTID *certid,
wget_http_response **response)
{
OCSP_REQUEST *ocspreq;
wget_http_response *resp;
unsigned char *ocspreq_bytes = NULL;
size_t ocspreq_bytes_len;
ocspreq = OCSP_REQUEST_new();
if (!ocspreq)
goto end;
if (!OCSP_request_add0_id(ocspreq, certid)) {
OCSP_REQUEST_free(ocspreq);
ocspreq = NULL;
goto end;
}
if (config.ocsp_nonce && !OCSP_request_add1_nonce(ocspreq, NULL, 0)) {
OCSP_REQUEST_free(ocspreq);
ocspreq = NULL;
goto end;
}
ocspreq_bytes_len = i2d_OCSP_REQUEST(ocspreq, &ocspreq_bytes);
if (!ocspreq_bytes || !ocspreq_bytes_len) {
OCSP_REQUEST_free(ocspreq);
ocspreq = NULL;
goto end;
}
resp = wget_http_get(
WGET_HTTP_URL, uri,
WGET_HTTP_SCHEME, "POST",
WGET_HTTP_HEADER_ADD, "Accept-Encoding", "identity",
WGET_HTTP_HEADER_ADD, "Accept", "*/*",
WGET_HTTP_HEADER_ADD, "Content-Type", "application/ocsp-request",
WGET_HTTP_MAX_REDIRECTIONS, 5,
WGET_HTTP_BODY, ocspreq_bytes, ocspreq_bytes_len,
0);
OPENSSL_free(ocspreq_bytes);
if (resp) {
*response = resp;
} else {
OCSP_REQUEST_free(ocspreq);
ocspreq = NULL;
}
end:
return ocspreq;
}
static const char *get_printable_ocsp_reason_desc(int reason)
{
switch (reason) {
case OCSP_REVOKED_STATUS_NOSTATUS:
return "not given";
case OCSP_REVOKED_STATUS_UNSPECIFIED:
return "unspecified";
case OCSP_REVOKED_STATUS_KEYCOMPROMISE:
return "key compromise";
case OCSP_REVOKED_STATUS_CACOMPROMISE:
return "CA compromise";
case OCSP_REVOKED_STATUS_AFFILIATIONCHANGED:
return "affiliation changed";
case OCSP_REVOKED_STATUS_SUPERSEDED:
return "superseded";
case OCSP_REVOKED_STATUS_CESSATIONOFOPERATION:
return "cessation of operation";
case OCSP_REVOKED_STATUS_CERTIFICATEHOLD:
return "certificate hold";
case OCSP_REVOKED_STATUS_REMOVEFROMCRL:
return "remove from CRL";
default:
return "unknown reason";
}
}
static void print_ocsp_response_status(int status)
{
char msg[64];
const char *status_string;
switch (status) {
case OCSP_RESPONSE_STATUS_SUCCESSFUL:
status_string = "successful";
break;
case OCSP_RESPONSE_STATUS_MALFORMEDREQUEST:
status_string = "malformed request";
break;
case OCSP_RESPONSE_STATUS_INTERNALERROR:
status_string = "internal error";
break;
case OCSP_RESPONSE_STATUS_TRYLATER:
status_string = "try later";
break;
case OCSP_RESPONSE_STATUS_SIGREQUIRED:
status_string = "signature required";
break;
case OCSP_RESPONSE_STATUS_UNAUTHORIZED:
status_string = "unauthorized";
break;
default:
wget_snprintf(msg, sizeof(msg), "unknown status code %d", status);
status_string = msg;
break;
}
debug_printf("*** OCSP response status: %s\n", status_string);
}
static void print_ocsp_cert_status(int status, int reason)
{
char msg[64];
const char *reason_string;
switch (status) {
case V_OCSP_CERTSTATUS_GOOD:
reason_string = "good";
break;
case V_OCSP_CERTSTATUS_UNKNOWN:
reason_string = "unknown";
break;
case V_OCSP_CERTSTATUS_REVOKED:
wget_snprintf(msg, sizeof(msg), "revoked (%s)", get_printable_ocsp_reason_desc(reason));
reason_string = msg;
break;
default:
reason_string = "invalid status code";
break;
}
debug_printf("*** OCSP cert status: %s\n", reason_string);
}
static void print_openssl_time(const char *prefix, const ASN1_GENERALIZEDTIME *t)
{
int nread;
char buf[128];
BIO *mem = BIO_new(BIO_s_mem());
ASN1_GENERALIZEDTIME_print(mem, t);
nread = BIO_read(mem, buf, sizeof(buf)-1);
if (nread > 0) {
buf[nread] = '\0';
debug_printf("%s%s\n", prefix, buf);
} else {
error_printf(_("ERROR: print_openssl_time: BIO_read failed\n"));
}
BIO_free_all(mem);
}
static int check_ocsp_response_times(const ASN1_GENERALIZEDTIME *thisupd,
const ASN1_GENERALIZEDTIME *nextupd)
{
int day, sec, retval = -1;
ASN1_TIME *now;
now = ASN1_TIME_adj(NULL, time(NULL), 0, 0);
if (!now) {
error_printf(_("Could not get current time!\n"));
return -1;
}
print_openssl_time("*** OCSP issued time: ", thisupd);
if (!nextupd) {
debug_printf("OCSP nextUpd not set. Checking thisUpd is not too old.\n");
if (!ASN1_TIME_diff(&day, &sec, now, thisupd)) {
error_printf(_("Could not compute time difference for thisUpd. Aborting.\n"));
goto end;
}
if (day < -3) {
error_printf(_("*** OCSP response thisUpd is too old. Aborting.\n"));
goto end;
}
retval = 0;
goto end;
}
print_openssl_time("*** OCSP update time: ", nextupd);
if (!ASN1_TIME_diff(&day, &sec, now, nextupd)) {
error_printf(_("Could not compute time difference for nextUpd. Aborting.\n"));
goto end;
}
if (day < 0 || (day == 0 && sec < 0)) {
error_printf(_("*** OCSP next update is in the past!\n"));
goto end;
}
retval = 0;
end:
ASN1_STRING_free(now);
return retval;
}
static int check_ocsp_response(OCSP_RESPONSE *ocspresp,
STACK_OF(X509) *certstack,
X509_STORE *certstore,
bool check_time)
{
int
retval = -1,
status, reason;
OCSP_BASICRESP *ocspbs = NULL;
OCSP_SINGLERESP *single;
ASN1_GENERALIZEDTIME *revtime = NULL,
*thisupd = NULL,
*nextupd = NULL;
status = OCSP_response_status(ocspresp);
print_ocsp_response_status(status);
if (status != OCSP_RESPONSE_STATUS_SUCCESSFUL) {
error_printf(_("Unsuccessful OCSP response\n"));
goto end;
}
if (!(ocspbs = OCSP_response_get1_basic(ocspresp)))
goto end;
if (!OCSP_basic_verify(ocspbs, certstack, certstore, 0)) {
error_printf(_("Could not verify OCSP certificate chain\n"));
goto end;
}
single = OCSP_resp_get0(ocspbs, 0);
if (!single) {
error_printf(_("Could not parse OCSP single response\n"));
goto end;
}
// thisupd and nextupd are internal pointers and MUST NOT be freed
status = OCSP_single_get0_status(single, &reason, &revtime, &thisupd, &nextupd);
if (status == -1) {
error_printf(_("Could not obtain OCSP response status\n"));
goto end;
}
print_ocsp_cert_status(status, reason);
if (status == V_OCSP_CERTSTATUS_REVOKED) {
print_openssl_time("*** Certificate revoked by OCSP at: ", revtime);
retval = 1; // Failure
goto end;
}
/* Check time is within an acceptable range */
if (check_time) {
if (!thisupd) {
error_printf(_("Could not get 'thisUpd' from OCSP response. Cannot check time.\n"));
goto end;
}
if (check_ocsp_response_times(thisupd, nextupd) < 0) {
retval = 1; // Failure
goto end;
}
}
retval = 0; // Success!
end:
if (ocspbs)
OCSP_BASICRESP_free(ocspbs);
return retval;
}
static int verify_ocsp(const char *ocsp_uri,
X509 *subject_cert, X509 *issuer_cert,
STACK_OF(X509) *certs, X509_STORE *certstore,
bool check_time, bool check_nonce)
{
int retval;
wget_http_response *resp;
const unsigned char *body;
OCSP_CERTID *certid;
OCSP_REQUEST *ocspreq;
OCSP_RESPONSE *ocspresp;
OCSP_BASICRESP *ocspbs = NULL;
/* Generate CertID and OCSP request */
certid = OCSP_cert_to_id(EVP_sha1(), subject_cert, issuer_cert);
if (!(ocspreq = send_ocsp_request(ocsp_uri,
certid,
&resp)))
return -1;
/* Check response */
body = (const unsigned char *) resp->body->data;
ocspresp = d2i_OCSP_RESPONSE(NULL, &body, resp->body->length);
if (!ocspresp) {
wget_http_free_response(&resp);
OCSP_REQUEST_free(ocspreq);
return -1;
}
if ((retval = check_ocsp_response(ocspresp, certs, certstore, check_time)) != 0)
goto end;
if (check_nonce) {
if (!(ocspbs = OCSP_response_get1_basic(ocspresp))) {
error_printf(_("Could not obtain OCSP_BASICRESPONSE\n"));
retval = -1;
goto end;
}
if (!OCSP_check_nonce(ocspreq, ocspbs)) {
error_printf(_("OCSP nonce does not match\n"));
retval = 1; // Failure
goto end;
}
OCSP_BASICRESP_free(ocspbs);
ocspbs = NULL;
}
retval = 0; // Success
end:
if (ocspbs)
OCSP_BASICRESP_free(ocspbs);
wget_http_free_response(&resp);
OCSP_RESPONSE_free(ocspresp);
OCSP_REQUEST_free(ocspreq);
return retval;
}
static char *read_ocsp_uri_from_certificate(X509 *cert)
{
STACK_OF(OPENSSL_STRING) *str_stack = X509_get1_ocsp(cert);
if (str_stack && sk_OPENSSL_STRING_num(str_stack) > 0) {
char *uri = wget_strdup(sk_OPENSSL_STRING_value(str_stack, 0));
X509_email_free(str_stack); // utterly misnamed, it simply frees a stack of strings.
return uri;
}
return NULL;
}
static char *compute_cert_fingerprint(X509 *cert)
{
/*
* OpenSSL does not provide a function that calculates the cert fingerprint directly
* (like GnuTLS' gnutls_x509_crt_get_fingerprint()), but we can code it away. Fingerprint
* is basically a SHA-256 hash of the DER-encoded certificate.
*/
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
char *hexstring = NULL;
unsigned char
*der_output = NULL,
*digest_output = NULL;
int
der_length,
digest_length,
hexstring_length;
if ((der_length = i2d_X509(cert, &der_output)) < 0)
goto bail;
/* Compute SHA-256 digest of the DER-encoded certificate */
digest_length = EVP_MD_size(EVP_sha256());
digest_output = wget_malloc(digest_length);
if (!digest_output)
goto bail;
if (!EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL))
goto bail;
if (!EVP_DigestUpdate(mdctx, der_output, der_length))
goto bail;
if (!EVP_DigestFinal_ex(mdctx, digest_output, NULL))
goto bail;
OPENSSL_free(der_output);
der_output = NULL;
EVP_MD_CTX_free(mdctx);
mdctx = NULL;
/* Convert SHA-256 digest to hex string */
hexstring_length = (digest_length * 2) + 1;
hexstring = wget_malloc(hexstring_length);
if (!hexstring)
goto bail;
wget_memtohex(digest_output, digest_length, hexstring, hexstring_length);
xfree(digest_output);
return hexstring;
bail:
xfree(hexstring);
xfree(digest_output);
if (der_output)
OPENSSL_free(der_output);
if (mdctx)
EVP_MD_CTX_free(mdctx);
return NULL;
}
static int check_cert_chain_for_ocsp(STACK_OF(X509) *certs, X509_STORE *store, const char *hostname)
{
wget_ocsp_stats_data stats;
int num_ok = 0, num_revoked = 0, num_ignored = 0, revoked, ocsp_ok;
char
*ocsp_uri = NULL,
*fingerprint;
X509 *cert, *issuer_cert;
unsigned cert_list_size = sk_X509_num(certs);
for (unsigned i = 0; i < cert_list_size; i++) {
cert = sk_X509_value(certs, i);
issuer_cert = sk_X509_value(certs, i+1);
if (!issuer_cert)
break;
/* Compute cert fingerprint */
fingerprint = compute_cert_fingerprint(cert);
if (!fingerprint) {
error_printf(_("Could not compute certificate fingerprint for cert %u\n"), i);
return 0; /* Treat this as an error */
}
/* Check if there's already an OCSP response stored in cache */
if (config.ocsp_cert_cache) {
if (wget_ocsp_fingerprint_in_cache(config.ocsp_cert_cache, fingerprint, &revoked)) {
/* Found cert's fingerprint in cache */
if (revoked) {
debug_printf("Certificate %u has been revoked (cached response)\n", i);
num_revoked++;
} else {
debug_printf("Certificate %u is valid (cached response)\n", i);
num_ok++;
}
xfree(fingerprint);
continue;
}
}
if (!config.ocsp_server) {
ocsp_uri = read_ocsp_uri_from_certificate(cert);
if (!ocsp_uri) {
debug_printf("OCSP URI not given and not found in certificate. Skipping OCSP check for cert %u.\n",
i);
num_ignored++;
xfree(fingerprint);
continue;
}
}
debug_printf("Contacting OCSP server. URI: %s\n",
config.ocsp_server ? config.ocsp_server : ocsp_uri);
ocsp_ok = verify_ocsp(config.ocsp_server ? config.ocsp_server : ocsp_uri,
cert, issuer_cert, certs, store,
config.ocsp_date, config.ocsp_nonce);
if (ocsp_ok == 0)
num_ok++;
else if (ocsp_ok == 1)
num_revoked++;
else
num_ignored++;
/* Add the certificate to the OCSP cache */
if (ocsp_ok == 0 || ocsp_ok == 1) {
wget_ocsp_db_add_fingerprint(config.ocsp_cert_cache,
fingerprint,
time(NULL) + 3600, /* Cache entry valid for 1 hour */
(ocsp_ok == 0)); /* valid? */
}
xfree(fingerprint);
xfree(ocsp_uri);
}
if (ocsp_stats_callback) {
stats.hostname = hostname;
stats.nvalid = num_ok;
stats.nrevoked = num_revoked;
stats.nignored = num_ignored;
stats.stapling = 0;
ocsp_stats_callback(&stats, ocsp_stats_ctx);
}
return (num_revoked == 0);
}
/*
* This is our custom revocation check function.
* It will be invoked by OpenSSL at some point during the TLS handshake.
* It takes the server's certificate chain, and its purpose is to check the revocation
* status for each certificate in it. We validate certs against HPKP and OCSP here.
* OpenSSL will make other checks before calling this function: cert signature, CRLs, etc.
* This function should return the value of 'ossl_retval' on success
* (which retains the result of previous checks made by OpenSSL) and 0 on failure (will override
* OpenSSL's result, whatever it is).
*/
static int openssl_revocation_check_fn(int ossl_retval, X509_STORE_CTX *storectx)
{
X509_STORE *store;
struct verification_flags *vflags;
STACK_OF(X509) *certs = X509_STORE_CTX_get1_chain(storectx);
if (ossl_retval == 0) {
/* ossl_retval == 0 means certificate was revoked by OpenSSL before entering this callback */
goto end;
}
store = X509_STORE_CTX_get0_store(storectx);
if (!store) {
error_printf(_("Could not retrieve certificate store. Will skip HPKP checks.\n"));
goto end;
}
vflags = X509_STORE_get_ex_data(store, store_userdata_idx);
if (!vflags) {
error_printf(_("Could not retrieve saved verification status flags.\n"));
goto end;
}
if (vflags->verifying_ocsp)
goto end;
/* Store the certificate chain size */
vflags->cert_chain_size = sk_X509_num(certs);
if (config.hpkp_cache) {
/* Check cert chain against HPKP database */
if (!check_cert_chain_for_hpkp(certs, vflags->hostname, &vflags->hpkp_stats)) {
error_printf(_("Public key pinning mismatch.\n"));
ossl_retval = 0;
goto end;
}
}
if (config.ocsp && !vflags->ocsp_checked) {
/* Check cert chain against OCSP */
vflags->verifying_ocsp = 1;
if (!check_cert_chain_for_ocsp(certs, store, vflags->hostname)) {
error_printf(_("Certificate revoked by OCSP.\n"));
ossl_retval = 0;
goto end;
}
vflags->ocsp_checked = 1;
vflags->verifying_ocsp = 0;
}
end:
sk_X509_pop_free(certs, X509_free);
return ossl_retval;
}
static int openssl_init(SSL_CTX *ctx)
{
int retval = 0;
X509_STORE *store;
if (!config.check_certificate) {
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
info_printf(_("Certificate check disabled. Peer's certificate will NOT be checked.\n"));
goto end;
}
store = SSL_CTX_get_cert_store(ctx);
if (!store) {
error_printf(_("OpenSSL: Could not obtain cert store\n"));
retval = WGET_E_UNKNOWN;
goto end;
}
if (config.ca_directory && *config.ca_directory) {
retval = openssl_load_trust_files(ctx, config.ca_directory);
if (retval < 0)
goto end;
if (config.crl_file) {
/* Load CRL file in PEM format. */
if ((retval = openssl_load_crl(store, config.crl_file)) < 0) {
error_printf(_("Could not load CRL from '%s' (%d)\n"),
config.crl_file,
retval);
goto end;
}
}
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);
}
/* Load individual CA file, if requested */
if (config.ca_file && *config.ca_file
&& !SSL_CTX_load_verify_locations(ctx, config.ca_file, NULL))
{
error_printf(_("Could not load CA certificate from file '%s'\n"), config.ca_file);
}
#ifdef WITH_OCSP
if (config.ocsp_stapling)
SSL_CTX_set_tlsext_status_cb(ctx, ocsp_resp_cb);
#endif
/* Set our custom revocation check function, for HPKP and OCSP validation */
X509_STORE_set_verify_cb(store, openssl_revocation_check_fn);
retval = openssl_set_priorities(ctx, config.secure_protocol);
end:
return retval;
}
static void openssl_deinit(SSL_CTX *ctx)
{
SSL_CTX_free(ctx);
}
/**
* Initialize the SSL/TLS engine as a client.
*
* This function assumes the caller is an SSL client connecting to a server.
* The functions wget_ssl_open(), wget_ssl_close() and wget_ssl_deinit() can be called
* after this.
*
* This is where the root certificates get loaded from the folder specified in the
* `WGET_SSL_CA_DIRECTORY` parameter. If any of the files in that folder cannot be loaded
* for whatever reason, that file will be silently skipped without harm (a message will be
* printed to the debug log stream).
*
* CLRs and private keys and their certificates are also loaded here.
*
* On systems with automatic library constructors/destructors, this function
* is thread-safe. On other systems it is not thread-safe.
*
* This function may be called several times. Only the first call really
* takes action.
*/
void wget_ssl_init(void)
{
wget_thread_mutex_lock(mutex);
if (!init) {
_ctx = SSL_CTX_new(TLS_client_method());
if (_ctx && openssl_init(_ctx) == 0) {
init++;
#ifdef LIBRESSL_VERSION_NUMBER
debug_printf("LibreSSL initialized\n");
#else
debug_printf("OpenSSL initialized\n");
#endif
} else {
error_printf(_("Could not initialize OpenSSL\n"));
}
}
wget_thread_mutex_unlock(mutex);
}
/**
* Deinitialize the SSL/TLS engine, after it has been initialized
* with wget_ssl_init().
*
* This function unloads everything that was loaded in wget_ssl_init().
*
* On systems with automatic library constructors/destructors, this function
* is thread-safe. On other systems it is not thread-safe.
*
* This function may be called several times. Only the last deinit really
* takes action.
*/
void wget_ssl_deinit(void)
{
wget_thread_mutex_lock(mutex);
if (init == 1)
openssl_deinit(_ctx);
if (init > 0)
init--;
wget_thread_mutex_unlock(mutex);
}
static int ssl_resume_session(SSL *ssl, const char *hostname)
{
void *sess = NULL;
size_t sesslen;
SSL_SESSION *ssl_session;
if (!config.tls_session_cache)
return 0;
if (wget_tls_session_get(config.tls_session_cache,
hostname,
&sess, &sesslen) == 0
&& sess)
{
debug_printf("Found cached session data for host '%s'\n",hostname);
ssl_session = d2i_SSL_SESSION(NULL,
(const unsigned char **) &sess,
(long) sesslen);
if (!ssl_session) {
error_printf(_("OpenSSL: Could not parse cached session data.\n"));
return -1;
}
#if OPENSSL_VERSION_NUMBER >= 0x10101000 && !defined LIBRESSL_VERSION_NUMBER
if (!SSL_SESSION_is_resumable(ssl_session))
return -1;
#endif
if (!SSL_set_session(ssl, ssl_session)) {
error_printf(_("OpenSSL: Could not set session data.\n"));
return -1;
}
SSL_SESSION_free(ssl_session);
return 1;
}
return 0;
}
static int ssl_save_session(const SSL *ssl, const char *hostname)
{
void *sess = NULL;
unsigned long sesslen;
SSL_SESSION *ssl_session = SSL_get0_session(ssl);
if (!ssl_session || !config.tls_session_cache)
return 0;
sesslen = i2d_SSL_SESSION(ssl_session, (unsigned char **) &sess);
if (sesslen) {
wget_tls_session_db_add(config.tls_session_cache,
wget_tls_session_new(hostname,
18 * 3600, /* session valid for 18 hours */
sess, sesslen));
OPENSSL_free(sess);
return 1;
}
return 0;
}
static int wait_2_read_and_write(int sockfd, int timeout)
{
int retval = wget_ready_2_transfer(sockfd,
timeout,
WGET_IO_READABLE | WGET_IO_WRITABLE);
if (retval == 0)
retval = WGET_E_TIMEOUT;
return retval;
}
static bool ssl_set_alpn_offering(SSL *ssl, const char *alpn)
{
int ret = WGET_E_UNKNOWN;
const char *s, *e;
char sbuf[32];
wget_buffer buf;
wget_buffer_init(&buf, sbuf, sizeof(sbuf));
for (s = e = alpn; *e; s = e + 1) {
if ((e = strchrnul(s, ',')) != s) {
if (e - s > 64) { // let's be reasonable
debug_printf("ALPN protocol too long %.*s\n", (int) (e - s), s);
continue;
}
debug_printf("ALPN offering %.*s\n", (int) (e - s), s);
wget_buffer_memset_append(&buf, (e - s) & 0x7F, 1); // length of protocol string
wget_buffer_memcat(&buf, s, e - s);
}
}
if (buf.length) {
if (SSL_set_alpn_protos(ssl, (unsigned char *) buf.data, (unsigned) buf.length)) {
debug_printf("OpenSSL: ALPN: Could not set ALPN offering");
} else
ret = WGET_E_SUCCESS;
}
wget_buffer_deinit(&buf);
return ret;
}
static void ssl_set_alpn_selected_protocol(const SSL *ssl, wget_tcp *tcp, wget_tls_stats_data *stats)
{
const unsigned char *data;
unsigned int datalen;
SSL_get0_alpn_selected(ssl, &data, &datalen);
if (data && datalen) {
debug_printf("ALPN: Server accepted protocol '%.*s'\n", (int) datalen, data);
/* Success - Set selected protocol and update stats */
if (stats)
stats->alpn_protocol = wget_strmemdup(data, datalen);
if (datalen == 2 && data[0] == 'h' && data[1] == '2') {
tcp->protocol = WGET_PROTOCOL_HTTP_2_0;
if (stats)
stats->http_protocol = WGET_PROTOCOL_HTTP_2_0;
}
}
}
static int get_tls_version(const SSL *ssl)
{
int version = SSL_version(ssl);
/*
* These values are mapped to the return values of GnuTLS' function
* gnutls_protocol_get_version() - integers on a gnutls_protocol_t enum.
*
* See: https://gitlab.com/gnutls/gnutls/blob/master/lib/includes/gnutls/gnutls.h.in#L736
*/
switch (version) {
case SSL3_VERSION:
/* SSL v3 */
return 1;
case TLS1_VERSION:
/* TLS 1.0 */
return 2;
case TLS1_1_VERSION:
/* TLS 1.1 */
return 3;
case TLS1_2_VERSION:
/* TLS 1.2 */
return 4;
#if defined TLS1_3_VERSION && TLS1_2_VERSION != TLS1_3_VERSION
case TLS1_3_VERSION:
/* TLS 1.3 */
return 5;
#endif
default:
return -1;
}
}
/**
* \param[in] tcp A TCP connection (see wget_tcp_init())
* \return `WGET_E_SUCCESS` on success or an error code (`WGET_E_*`) on failure
*
* Run an SSL/TLS handshake.
*
* This functions establishes an SSL/TLS tunnel (performs an SSL/TLS handshake)
* over an active TCP connection. A pointer to the (internal) SSL/TLS session context
* can be found in `tcp->ssl_session` after successful execution of this function. This pointer
* has to be passed to wget_ssl_close() to close the SSL/TLS tunnel.
*
* If the handshake cannot be completed in the specified timeout for the provided TCP connection
* this function fails and returns `WGET_E_TIMEOUT`. You can set the timeout with wget_tcp_set_timeout().
*/
int wget_ssl_open(wget_tcp *tcp)
{
SSL *ssl = NULL;
X509_STORE *store;
int retval, error, resumed;
struct verification_flags *vflags = NULL;
wget_tls_stats_data stats = {
.alpn_protocol = NULL,
.version = -1,
.false_start = 0,
.tfo = 0,
.resumed = 0,
.http_protocol = WGET_PROTOCOL_HTTP_1_1,
.cert_chain_size = 0
}, *stats_p = NULL;
if (!tcp || tcp->sockfd < 0)
return WGET_E_INVALID;
if (!init)
wget_ssl_init();
/* Initiate a new TLS connection from an existing OpenSSL context */
if (!(ssl = SSL_new(_ctx)) || !SSL_set_fd(ssl, FD_TO_SOCKET(tcp->sockfd))) {
retval = WGET_E_UNKNOWN;
goto bail;
}
/* Store state flags for the verification callback */
vflags = wget_malloc(sizeof(struct verification_flags));
if (!vflags) {
retval = WGET_E_MEMORY;
goto bail;
}
vflags->ocsp_checked = 0;
vflags->verifying_ocsp = 0;
vflags->cert_chain_size = 0;
vflags->hostname = tcp->ssl_hostname;
if (store_userdata_idx == -1) {
retval = WGET_E_UNKNOWN;
goto bail;
}
store = SSL_CTX_get_cert_store(_ctx);
if (!store) {
retval = WGET_E_UNKNOWN;
goto bail;
}
vflags->certstore = store;
vflags->ssl = ssl;
if (!X509_STORE_set_ex_data(store, store_userdata_idx, (void *) vflags)) {
retval = WGET_E_UNKNOWN;
goto bail;
}
#ifdef WITH_OCSP
SSL_CTX_set_tlsext_status_arg(_ctx, vflags);
#endif
/* Enable stats logging, if requested */
if (tls_stats_callback)
stats_p = &stats;
/* Enable host name verification, if requested */
if (config.check_hostname) {
SSL_set1_host(ssl, tcp->ssl_hostname);
#ifndef LIBRESSL_VERSION_NUMBER
// LibreSSL <= 3.0.2 does not know SSL_set_hostflags()
SSL_set_hostflags(ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
#endif
}
#if !defined LIBRESSL_VERSION_NUMBER || !defined X509_CHECK_FLAG_NEVER_CHECK_SUBJECT
// LibreSSL <= 3.0.2 does not know SSL_set_hostflags() nor X509_CHECK_FLAG_NEVER_CHECK_SUBJECT
// OpenSSL < 1.1 doesn't have X509_CHECK_FLAG_NEVER_CHECK_SUBJECT
else {
SSL_set_hostflags(ssl, X509_CHECK_FLAG_NEVER_CHECK_SUBJECT);
info_printf(_("Host name check disabled. Server certificate's subject name will not be checked.\n"));
}
#endif
#ifdef WITH_OCSP
if (config.ocsp_stapling) {
if (SSL_set_tlsext_status_type(ssl, TLSEXT_STATUSTYPE_ocsp))
debug_printf("Sending 'status_request' extension in handshake\n");
else
error_printf(_("Could not set 'status_request' extension\n"));
}
#endif
/* Send Server Name Indication (SNI) */
if (tcp->ssl_hostname && !SSL_set_tlsext_host_name(ssl, tcp->ssl_hostname))
error_printf(_("SNI could not be sent"));
/* Send ALPN if requested */
if (config.alpn && ssl_set_alpn_offering(ssl, config.alpn))
error_printf(_("ALPN offering could not be sent"));
/* Resume from a previous SSL/TLS session, if available */
if ((resumed = ssl_resume_session(ssl, tcp->ssl_hostname)) == 1)
debug_printf("Will try to resume cached TLS session");
else if (resumed == 0)
debug_printf("No cached TLS session available. Will run a full handshake.");
else
error_printf(_("Could not get cached TLS session"));
do {
/* Wait for socket to become ready */
if (tcp->connect_timeout &&
(retval = wait_2_read_and_write(tcp->sockfd, tcp->connect_timeout)) < 0)
goto bail;
/* Run TLS handshake */
retval = SSL_connect(ssl);
if (retval > 0) {
error = 0;
resumed = SSL_session_reused(ssl);
break;
}
error = SSL_get_error(ssl, retval);
} while (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE);
if (retval <= 0) {
/* Error! Tell the user what happened, and exit. */
if (error == SSL_ERROR_SSL) {
error_printf(_("Could not complete TLS handshake: %s\n"),
ERR_reason_error_string(ERR_peek_last_error()));
}
/* Return proper error code - Most of the time this will be a cert validation error */
retval = (ERR_GET_REASON(ERR_peek_last_error()) == SSL_R_CERTIFICATE_VERIFY_FAILED ?
WGET_E_CERTIFICATE :
WGET_E_HANDSHAKE);
goto bail;
}
/* Success! */
debug_printf("Handshake completed%s\n", resumed ? " (resumed session)" : " (full handshake - not resumed)");
/* Save the current TLS session */
if (ssl_save_session(ssl, tcp->ssl_hostname))
debug_printf("TLS session saved in cache");
else
debug_printf("TLS session discarded");
/* Set the protocol selected by the server via ALPN, if any */
if (config.alpn)
ssl_set_alpn_selected_protocol(ssl, tcp, stats_p);
if (stats_p) {
stats_p->version = get_tls_version(ssl);
stats_p->hostname = vflags->hostname;
stats_p->resumed = resumed;
stats_p->cert_chain_size = vflags->cert_chain_size;
tls_stats_callback(stats_p, tls_stats_ctx);
xfree(stats_p->alpn_protocol);
#ifdef MSG_FASTOPEN
stats_p->tfo = wget_tcp_get_tcp_fastopen(tcp);
#endif
}
tcp->hpkp = vflags->hpkp_stats;
tcp->ssl_session = ssl;
xfree(vflags);
return WGET_E_SUCCESS;
bail:
if (stats_p)
xfree(stats_p->alpn_protocol);
xfree(vflags);
if (ssl)
SSL_free(ssl);
return retval;
}
/**
* \param[in] session The SSL/TLS session (a pointer to it), which is located at the `ssl_session` field
* of the TCP connection (see wget_ssl_open()).
*
* Close an active SSL/TLS tunnel, which was opened with wget_ssl_open().
*
* The underlying TCP connection is kept open.
*/
void wget_ssl_close(void **session)
{
SSL *ssl;
int retval;
if (session && *session) {
ssl = *session;
do
retval = SSL_shutdown(ssl);
while (retval == 0);
SSL_free(ssl);
*session = NULL;
}
}
static int ssl_transfer(int want,
void *session, int timeout,
void *buf, int count)
{
SSL *ssl;
int fd, retval, error, ops = want;
if (count == 0)
return 0;
if ((ssl = session) == NULL)
return WGET_E_INVALID;
if ((fd = SOCKET_TO_FD(SSL_get_fd(ssl))) < 0)
return WGET_E_UNKNOWN;
if (timeout < -1)
timeout = -1;
do {
if (timeout) {
/* Wait until file descriptor becomes ready */
retval = wget_ready_2_transfer(fd, timeout, ops);
if (retval < 0)
return retval;
else if (retval == 0)
return WGET_E_TIMEOUT;
}
/* We assume socket is non-blocking so neither of these should block */
if (want == WGET_IO_READABLE)
retval = SSL_read(ssl, buf, count);
else
retval = SSL_write(ssl, buf, count);
if (retval < 0) {
error = SSL_get_error(ssl, retval);
if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE) {
/* Socket not ready - let's try again (unless timeout was zero) */
ops = WGET_IO_WRITABLE | WGET_IO_READABLE;
if (timeout == 0)
return 0;
} else {
/* Not exactly a handshake error, but this is the closest one to signal TLS layer errors */
return WGET_E_HANDSHAKE;
}
}
} while (retval < 0);
return retval;
}
/**
* \param[in] session An opaque pointer to the SSL/TLS session (obtained with wget_ssl_open() or wget_ssl_server_open())
* \param[in] buf Destination buffer where the read data will be placed
* \param[in] count Length of the buffer \p buf
* \param[in] timeout The amount of time to wait until data becomes available (in milliseconds)
* \return The number of bytes read, or a negative value on error.
*
* Read data from the SSL/TLS tunnel.
*
* This function will read at most \p count bytes, which will be stored
* in the buffer \p buf.
*
* The \p timeout parameter tells how long to wait until some data becomes
* available to read. A \p timeout value of zero causes this function to return
* immediately, whereas a negative value will cause it to wait indefinitely.
* This function returns the number of bytes read, which may be zero if the timeout elapses
* without any data having become available.
*
* If a rehandshake is needed, this function does it automatically and tries
* to read again.
*/
ssize_t wget_ssl_read_timeout(void *session,
char *buf, size_t count,
int timeout)
{
int retval = ssl_transfer(WGET_IO_READABLE, session, timeout, buf, (int) count);
if (retval == WGET_E_HANDSHAKE) {
error_printf(_("TLS read error: %s\n"),
ERR_reason_error_string(ERR_peek_last_error()));
retval = WGET_E_UNKNOWN;
}
return retval;
}
/**
* \param[in] session An opaque pointer to the SSL/TLS session (obtained with wget_ssl_open() or wget_ssl_server_open())
* \param[in] buf Buffer with the data to be sent
* \param[in] count Length of the buffer \p buf
* \param[in] timeout The amount of time to wait until data can be sent to the wire (in milliseconds)
* \return The number of bytes written, or a negative value on error.
*
* Send data through the SSL/TLS tunnel.
*
* This function will write \p count bytes from \p buf.
*
* The \p timeout parameter tells how long to wait until data can be finally sent
* over the SSL/TLS tunnel. A \p timeout value of zero causes this function to return
* immediately, whereas a negative value will cause it to wait indefinitely.
* This function returns the number of bytes sent, which may be zero if the timeout elapses
* before any data could be sent.
*
* If a rehandshake is needed, this function does it automatically and tries
* to write again.
*/
ssize_t wget_ssl_write_timeout(void *session,
const char *buf, size_t count,
int timeout)
{
int retval = ssl_transfer(WGET_IO_WRITABLE, session, timeout, (void *) buf, (int) count);
if (retval == WGET_E_HANDSHAKE) {
error_printf(_("TLS write error: %s\n"),
ERR_reason_error_string(ERR_peek_last_error()));
retval = WGET_E_UNKNOWN;
}
return retval;
}
/**
* \param[in] fn A `wget_ssl_stats_callback_tls` callback function to receive TLS statistics data
* \param[in] ctx Context data given to \p fn
*
* Set callback function to be called when TLS statistics are available
*/
void wget_ssl_set_stats_callback_tls(wget_tls_stats_callback fn, void *ctx)
{
tls_stats_callback = fn;
tls_stats_ctx = ctx;
}
/**
* \param[in] fn A `wget_ssl_stats_callback_ocsp` callback function to receive OCSP statistics data
* \param[in] ctx Context data given to \p fn
*
* Set callback function to be called when OCSP statistics are available
*/
void wget_ssl_set_stats_callback_ocsp(wget_ocsp_stats_callback fn, void *ctx)
{
ocsp_stats_callback = fn;
ocsp_stats_ctx = ctx;
}
/** @} */