/* * Copyright (c) 2019-2024 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 . * * * WolfSSL integration * * Resources: * https://github.com/wolfSSL/wolfssl-examples * RFC6066 Transport Layer Security (TLS) Extensions: Extension Definitions (defines OCSP stapling) * RFC6960 Online Certificate Status Protocol - OCSP * RFC6961 TLS Multiple Certificate Status Request Extension * * Testing revocation: * https://revoked.grc.com/ * https://test-sspev.verisign.com:2443/test-SSPEV-revoked-verisign.html * */ #include #include #include #include #include #ifdef _WIN32 # include #else # define FD_TO_SOCKET(x) (x) # define SOCKET_TO_FD(x) (x) #endif #ifndef DEBUG_WOLFSSL #include #undef DEBUG_WOLFSSL #else #include #endif #include #include #include "private.h" #include "net.h" #include "filename.h" /** * \file * \brief Functions for establishing and managing SSL/TLS connections * \defgroup libwget-ssl SSL/TLS engine * * @{ */ 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, report_invalid_cert : 1, check_hostname : 1, print_info : 1, ocsp : 1, ocsp_stapling : 1; } config = { .check_certificate = 1, .report_invalid_cert = 1, .check_hostname = 1, .ocsp = 0, .ocsp_stapling = 1, .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", .ca_file = "system", #ifdef WITH_LIBNGHTTP2 .alpn = "h2,http/1.1", #endif }; struct session_context { const char * hostname; wget_hpkp_stats_result stats_hpkp; unsigned char ocsp_stapling : 1, valid : 1, delayed_session_data : 1; }; static WOLFSSL_CTX *ssl_ctx; #define error_printf_check(...) if (config.report_invalid_cert) wget_error_printf(__VA_ARGS__) /** * \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 config key %d (or value must not be a string)\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 config key %d (or value must not be an object)\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: whether to print (1) errors/warnings regarding certificate validation or not (0) * - 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). */ void wget_ssl_set_config_int(int key, int value) { switch (key) { case WGET_SSL_CHECK_CERTIFICATE: config.check_certificate = (char)value; break; case WGET_SSL_REPORT_INVALID_CERT: config.report_invalid_cert = (char)value; break; case WGET_SSL_CHECK_HOSTNAME: config.check_hostname = (char)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_PRINT_INFO: config.print_info = (char)value; break; case WGET_SSL_OCSP: config.ocsp = (char)value; break; case WGET_SSL_OCSP_STAPLING: config.ocsp_stapling = (char)value; break; default: error_printf(_("Unknown config key %d (or value must not be an integer)\n"), key); } } /* This function will verify the peer's certificate, and check * if the hostname matches, as well as the activation, expiration dates. */ /* static int verify_certificate_callback(gnutls_session_t session) { unsigned int status, deinit_cert = 0, deinit_issuer = 0; const gnutls_datum_t *cert_list = 0; unsigned int cert_list_size; int ret = -1, err, ocsp_ok = 0, pinning_ok = 0; gnutls_x509_crt_t cert = NULL, issuer = NULL; const char *hostname; const char *tag = config.check_certificate ? _("ERROR") : _("WARNING"); unsigned nvalid = 0, nrevoked = 0, nignored = 0; // read hostname struct session_context *ctx = gnutls_session_get_ptr(session); hostname = ctx->hostname; // This verification function uses the trusted CAs in the credentials // structure. So you must have installed one or more CA certificates. // if (gnutls_certificate_verify_peers3(session, hostname, &status) != GNUTLS_E_SUCCESS) { // if (wget_get_logger(WGET_LOGGER_DEBUG)) // print_info(session); error_printf_check(_("%s: Certificate verification error\n"), tag); goto out; } // if (wget_get_logger(WGET_LOGGER_DEBUG)) // print_info(session); if (status & GNUTLS_CERT_REVOKED) { if (config.ocsp_cert_cache) wget_ocsp_db_add_host(config.ocsp_cert_cache, hostname, 0); // remove entry from cache if (ctx->ocsp_stapling) { if (gnutls_x509_crt_init(&cert) == GNUTLS_E_SUCCESS) { if ((cert_list = gnutls_certificate_get_peers(session, &cert_list_size))) { if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) == GNUTLS_E_SUCCESS) { add_cert_to_ocsp_cache(cert, 0); } } gnutls_x509_crt_deinit(cert); } } } if (status) { gnutls_datum_t out; if (gnutls_certificate_verification_status_print( status, gnutls_certificate_type_get(session), &out, 0) == GNUTLS_E_SUCCESS) { error_printf_check("%s: %s\n", tag, out.data); // no translation gnutls_free(out.data); } goto out; } // Up to here the process is the same for X.509 certificates and // OpenPGP keys. From now on X.509 certificates are assumed. This can // be easily extended to work with openpgp keys as well. // if (gnutls_certificate_type_get(session) != GNUTLS_CRT_X509) { error_printf_check(_("%s: Certificate must be X.509\n"), tag); goto out; } if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS) { error_printf_check(_("%s: Error initializing X.509 certificate\n"), tag); goto out; } deinit_cert = 1; if (!(cert_list = gnutls_certificate_get_peers(session, &cert_list_size))) { error_printf_check(_("%s: No certificate was found!\n"), tag); goto out; } if ((err = gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER)) != GNUTLS_E_SUCCESS) { error_printf_check(_("%s: Failed to parse certificate: %s\n"), tag, gnutls_strerror (err)); goto out; } if (!config.check_hostname || (config.check_hostname && hostname && gnutls_x509_crt_check_hostname(cert, hostname))) ret = 0; else goto out; // At this point, the cert chain has been found valid regarding the locally available CA certificates and CRLs. // Now, we are going to check the revocation status via OCSP if (config.ocsp_stapling) { if (!ctx->valid && ctx->ocsp_stapling) { if (gnutls_ocsp_status_request_is_checked(session, 0)) { debug_printf("Server certificate is valid regarding OCSP stapling\n"); // get_cert_fingerprint(cert, fingerprint, sizeof(fingerprint)); // calc hexadecimal fingerprint string add_cert_to_ocsp_cache(cert, 1); nvalid = 1; } else if (gnutls_ocsp_status_request_is_checked(session, GNUTLS_OCSP_SR_IS_AVAIL)) error_printf_check(_("WARNING: The certificate's (stapled) OCSP status is invalid\n")); else if (!config.ocsp) error_printf_check(_("WARNING: The certificate's (stapled) OCSP status has not been sent\n")); } else if (ctx->valid) debug_printf("OCSP: Host '%s' is valid (from cache)\n", hostname); } for (unsigned it = 0; it < cert_list_size; it++) { gnutls_x509_crt_deinit(cert); gnutls_x509_crt_init(&cert); if ((err = gnutls_x509_crt_import(cert, &cert_list[it], GNUTLS_X509_FMT_DER)) != GNUTLS_E_SUCCESS) { error_printf_check(_("%s: Failed to parse certificate[%u]: %s\n"), tag, it, gnutls_strerror (err)); continue; } if (cert_verify_hpkp(cert, hostname, session) == 0) pinning_ok = 1; cert_verify_hpkp(cert, hostname, session); if (config.ocsp && it > nvalid) { char fingerprint[64 * 2 +1]; int revoked; get_cert_fingerprint(cert, fingerprint, sizeof(fingerprint)); // calc hexadecimal fingerprint string if (wget_ocsp_fingerprint_in_cache(config.ocsp_cert_cache, fingerprint, &revoked)) { // found cert's fingerprint in cache if (revoked) { debug_printf("Certificate[%u] of '%s' has been revoked (cached)\n", it, hostname); nrevoked++; } else { debug_printf("Certificate[%u] of '%s' is valid (cached)\n", it, hostname); nvalid++; } continue; } if (deinit_issuer) { gnutls_x509_crt_deinit(issuer); deinit_issuer = 0; } if ((err = gnutls_certificate_get_issuer(credentials, cert, &issuer, 0)) != GNUTLS_E_SUCCESS && it < cert_list_size - 1) { gnutls_x509_crt_init(&issuer); deinit_issuer = 1; if ((err = gnutls_x509_crt_import(issuer, &cert_list[it + 1], GNUTLS_X509_FMT_DER)) != GNUTLS_E_SUCCESS) { debug_printf("Decoding error: %s\n", gnutls_strerror(err)); continue; } } else if (err != GNUTLS_E_SUCCESS) { debug_printf("Cannot find issuer: %s\n", gnutls_strerror(err)); continue; } ocsp_ok = cert_verify_ocsp(cert, issuer); debug_printf("check_ocsp_response() returned %d\n", ocsp_ok); if (ocsp_ok == 1) { debug_printf("Certificate[%u] of '%s' is valid (via OCSP)\n", it, hostname); wget_ocsp_db_add_fingerprint(config.ocsp_cert_cache, fingerprint, time(NULL) + 3600, 1); // 1h valid nvalid++; } else if (ocsp_ok == 0) { debug_printf("%s: Certificate[%u] of '%s' has been revoked (via OCSP)\n", tag, it, hostname); wget_ocsp_db_add_fingerprint(config.ocsp_cert_cache, fingerprint, time(NULL) + 3600, 0); // cert has been revoked nrevoked++; } else { debug_printf("WARNING: OCSP response not available or ignored\n"); nignored++; } } } if (config.ocsp && stats_callback_ocsp) { wget_ocsp_stats_data stats; stats.hostname = hostname; stats.nvalid = nvalid; stats.nrevoked = nrevoked; stats.nignored = nignored; stats.stapling = ctx->ocsp_stapling; stats_callback_ocsp(&stats); } if (config.ocsp_stapling || config.ocsp) { if (nvalid == cert_list_size) { wget_ocsp_db_add_host(config.ocsp_cert_cache, hostname, time(NULL) + 3600); // 1h valid } else if (nrevoked) { wget_ocsp_db_add_host(config.ocsp_cert_cache, hostname, 0); // remove entry from cache ret = -1; } } if (!pinning_ok) { error_printf_check(_("%s: Pubkey pinning mismatch!\n"), tag); ret = -1; } // 0: continue handshake // else: stop handshake out: if (deinit_cert) gnutls_x509_crt_deinit(cert); if (deinit_issuer) gnutls_x509_crt_deinit(issuer); return config.check_certificate ? ret : 0; } */ static int init; static wget_thread_mutex mutex; static void tls_exit(void) { if (mutex) wget_thread_mutex_destroy(&mutex); } INITIALIZER(tls_init) { if (!mutex) { wget_thread_mutex_init(&mutex); #ifdef DEBUG_WOLFSSL wolfSSL_Debugging_ON(); #endif // DEBUG_WOLFSSL // Initialize paths while in a thread-safe environment (mostly for _WIN32). wget_ssl_default_cert_dir(); wget_ssl_default_ca_bundle_path(); atexit(tls_exit); } } /* static void set_credentials(gnutls_certificate_credentials_t *credentials) { if (config.cert_file && !config.key_file) { // Use the private key from the cert file unless otherwise specified. config.key_file = config.cert_file; config.key_type = config.cert_type; } else if (!config.cert_file && config.key_file) { // Use the cert from the private key file unless otherwise specified. config.cert_file = config.key_file; config.cert_type = config.key_type; } if (config.cert_file && config.key_file) { if (config.key_type !=config.cert_type) { // GnuTLS can't handle this error_printf(_("GnuTLS requires the key and the cert to be of the same type.\n")); } if (gnutls_certificate_set_x509_key_file(*credentials,config.cert_file,config.key_file,key_type(config.key_type)) != GNUTLS_E_SUCCESS) error_printf(_("No certificates or keys were found\n")); } if (config.ca_file) { if (gnutls_certificate_set_x509_trust_file(*credentials, config.ca_file, key_type(config.ca_type)) <= 0) error_printf(_("No CAs were found in '%s'\n"), config.ca_file); } } */ /** * 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) { tls_init(); wget_thread_mutex_lock(mutex); if (!init) { WOLFSSL_METHOD *method; int min_version = -1; const char *ciphers = NULL; #ifdef DEBUG_WOLFSSL if (!wget_logger_is_active(wget_get_logger(WGET_LOGGER_DEBUG))) wolfSSL_Debugging_OFF(); #endif // DEBUG_WOLFSSL debug_printf("WolfSSL init\n"); wolfSSL_Init(); if (!wget_strcasecmp_ascii(config.secure_protocol, "SSLv2")) { #ifdef HAVE_SSLV2_CLIENT_METHOD method = SSLv2_client_method(); #else method = wolfSSLv23_client_method(); min_version = WOLFSSL_SSLV3; // SSLv2 is no longer supported, use SSLv3 as minimum #endif } else if (!wget_strcasecmp_ascii(config.secure_protocol, "SSLv3")) { method = wolfSSLv23_client_method(); min_version = WOLFSSL_SSLV3; } else if (!wget_strcasecmp_ascii(config.secure_protocol, "TLSv1")) { method = wolfSSLv23_client_method(); min_version = WOLFSSL_TLSV1; } else if (!wget_strcasecmp_ascii(config.secure_protocol, "TLSv1_1")) { method = wolfSSLv23_client_method(); min_version = WOLFSSL_TLSV1_1; } else if (!wget_strcasecmp_ascii(config.secure_protocol, "TLSv1_2")) { method = wolfSSLv23_client_method(); min_version = WOLFSSL_TLSV1_2; } else if (!wget_strcasecmp_ascii(config.secure_protocol, "TLSv1_3")) { method = wolfSSLv23_client_method(); min_version = WOLFSSL_TLSV1_3; } else if (!wget_strcasecmp_ascii(config.secure_protocol, "PFS")) { method = wolfSSLv23_client_method(); ciphers = "HIGH:!aNULL:!RC4:!MD5:!SRP:!PSK:!kRSA"; } else if (!wget_strcasecmp_ascii(config.secure_protocol, "auto")) { method = wolfSSLv23_client_method(); min_version = WOLFSSL_TLSV1_2; ciphers = "HIGH:!aNULL:!RC4:!MD5:!SRP:!PSK"; } else if (*config.secure_protocol) { method = wolfSSLv23_client_method(); ciphers = config.secure_protocol; } else { error_printf(_("Missing TLS method\n")); goto out; } /* Create and initialize WOLFSSL_CTX */ if ((ssl_ctx = wolfSSL_CTX_new(method)) == NULL) { error_printf(_("Failed to create WOLFSSL_CTX\n")); goto out; } if (min_version != -1) wolfSSL_CTX_SetMinVersion(ssl_ctx, min_version); /* int rc; char cipher_list[8096]; rc = wolfSSL_get_ciphers(cipher_list, (int) sizeof(cipher_list)); debug_printf("%d ciphers found %s (len=%zu)\n", rc, cipher_list, strlen(cipher_list)); */ if (ciphers) if (!wolfSSL_CTX_set_cipher_list(ssl_ctx, ciphers)) error_printf(_("WolfSSL: Failed to set ciphers '%s'\n"), ciphers); wolfSSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_NONE, NULL); if (config.check_certificate) { wolfSSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, NULL); bool system_certs_loaded = false; #ifdef WOLFSSL_SYS_CA_CERTS if (!wget_strcmp(config.ca_directory, "system") || !wget_strcmp(config.ca_file, "system")) { if (wolfSSL_CTX_load_system_CA_certs(ssl_ctx) != WOLFSSL_SUCCESS) { error_printf(_("Failed to load system certs\n")); } else { system_certs_loaded = true; debug_printf("System certificates loaded\n"); } } #endif const char *dir = config.ca_directory; const char *file = config.ca_file; debug_printf("Certificates %s %s\n", dir, file); if (dir && !system_certs_loaded && !wget_strcmp(dir, "system")) { dir = wget_ssl_default_cert_dir(); } if (file && !system_certs_loaded && !wget_strcmp(file, "system")) { file = wget_ssl_default_ca_bundle_path(); } if (dir && access(dir, F_OK)) dir = NULL; if (file && access(file, F_OK)) file = NULL; if (dir == NULL && file == NULL) { if (!system_certs_loaded) error_printf(_("Skipped loading CA certs. SSL verification will likely fail.\n")); goto out; } // Load client certificates into WOLFSSL_CTX if (wolfSSL_CTX_load_verify_locations(ssl_ctx, file, dir) != SSL_SUCCESS) { error_printf(_("Failed to load CA pem: %s or cert dir: %s, SSL verification will likely fail.\n"), file, dir); goto out; } else { if (dir) debug_printf("Certificates loaded from %s\n", dir); if (file) debug_printf("Certificates loaded from %s\n", file); } } /* if (config.crl_file) { WOLFSSL_X509_STORE *store = wolfSSL_CTX_get_cert_store(ssl_ctx); WOLFSSL_X509_LOOKUP *lookup; if (!(lookup = wolfSSL_X509_STORE_add_lookup(store, wolfSSL_X509_LOOKUP_file())) || (!X509_load_crl_file(lookup, config.crl_file, X509_FILETYPE_PEM))) return; wolfSSL_X509_STORE_set_flags(store, WOLFSSL_CRL_CHECK | WOLFSSL_CRL_CHECKALL); } */ out: init++; debug_printf("WolfSSL init done\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) { wolfSSL_CTX_free(ssl_ctx); ssl_ctx = NULL; wolfSSL_Cleanup(); } if (init > 0) init--; wget_thread_mutex_unlock(mutex); } static int do_handshake(WOLFSSL *session, int sockfd, int timeout) { int ret; // Wait for socket being ready before we call gnutls_handshake(). // I had problems on a KVM Win7 + CygWin (gnutls 3.2.4-1). int rc = wget_ready_2_write(sockfd, timeout); if (rc == 0) ret = WGET_E_TIMEOUT; else ret = WGET_E_HANDSHAKE; // Perform the TLS handshake while (rc > 0) { rc = wolfSSL_connect(session); if (rc == SSL_SUCCESS) { ret = WGET_E_SUCCESS; break; } rc = wolfSSL_get_error(session, rc); debug_printf("wolfSSL_connect2: (%d) (errno=%d) %s\n", rc, errno, wolfSSL_ERR_reason_error_string(rc)); /* if (rc == GNUTLS_E_CERTIFICATE_ERROR) { ret = WGET_E_CERTIFICATE; } else if (rc == GNUTLS_E_PUSH_ERROR && (errno == ECONNREFUSED || errno == ENOTCONN)) { // ECONNREFUSED: on Linux // ENOTCONN: MinGW (in out Gitlab CI runner) ret = WGET_E_CONNECT; } else if (rc == GNUTLS_E_PULL_ERROR && errno == 61) { // ENODATA, but not on OSX/Travis ? // We see this with older versions of GnuTLS, e.g. on TravisCI. (Tim, 11.4.2018) // It happens when trying to connect to a port without a listener ret = WGET_E_CONNECT; } else if (rc == GNUTLS_E_PREMATURE_TERMINATION && errno == EAGAIN) { // It happens when trying to connect to a closed port ret = WGET_E_CONNECT; } else if (rc == GNUTLS_E_UNEXPECTED_PACKET_LENGTH && errno == EAGAIN) { // We see this with older versions of GnuTLS, e.g. on TravisCI. (Tim, 11.4.2018) // It happens when trying to connect to a port without a listener ret = WGET_E_CONNECT; } else ret = WGET_E_HANDSHAKE; */ if (rc == WOLFSSL_ERROR_WANT_WRITE) { // wait for writeability rc = wget_ready_2_write(sockfd, timeout); } else if (rc == WOLFSSL_ERROR_WANT_READ) { // wait for readability rc = wget_ready_2_read(sockfd, timeout); } else { ret = WGET_E_CONNECT; break; } } return ret; } static void ShowX509(WOLFSSL_X509 *x509, const char *hdr) { char *altName; char *issuer; char *subject; byte serial[32]; int ret; int sz = sizeof(serial); if (!x509) { debug_printf("%s No Cert\n", hdr); return; } issuer = wolfSSL_X509_NAME_oneline(wolfSSL_X509_get_issuer_name(x509), NULL, 0); subject = wolfSSL_X509_NAME_oneline(wolfSSL_X509_get_subject_name(x509), NULL, 0); debug_printf("%s issuer : %s subject: %s", hdr, issuer, subject); while ((altName = wolfSSL_X509_get_next_altname(x509))) debug_printf(" altname = %s\n", altName); ret = wolfSSL_X509_get_serial_number(x509, serial, &sz); if (ret == WOLFSSL_SUCCESS) { char serialMsg[sizeof(serial) * 4 + 1]; // testsuite has multiple threads writing to stdout, get output // message ready to write once for (int i = 0; i < sz; i++) sprintf(serialMsg + (i * 4), ":%02x ", serial[i]); debug_printf(" serial number%s\n", serialMsg); } XFREE(subject, 0, DYNAMIC_TYPE_OPENSSL); XFREE(issuer, 0, DYNAMIC_TYPE_OPENSSL); { WOLFSSL_BIO* bio; char buf[256]; /* should be size of ASN_NAME_MAX */ int textSz; /* print out domain component if certificate has it */ textSz = wolfSSL_X509_NAME_get_text_by_NID( wolfSSL_X509_get_subject_name(x509), NID_domainComponent, buf, sizeof(buf)); if (textSz > 0) { debug_printf("Domain Component = %s\n", buf); } bio = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); if (bio) { wolfSSL_BIO_set_fp(bio, stdout, BIO_NOCLOSE); if (wget_logger_is_active(wget_get_logger(WGET_LOGGER_DEBUG))) wolfSSL_X509_print(bio, x509); wolfSSL_BIO_free(bio); } } } static void ShowX509Chain(WOLFSSL_X509_CHAIN *chain, int count, const char *hdr) { // int i; // int length; // unsigned char buffer[3072]; for (int i = 0; i < count; i++) { // wolfSSL_get_chain_cert_pem(chain, i, buffer, sizeof(buffer), &length); // buffer[length] = 0; // debug_printf("\n%s: %d has length %d data = \n%s\n", hdr, i, length, buffer); WOLFSSL_X509 *chainX509 = wolfSSL_get_chain_X509(chain, i); if (chainX509) ShowX509(chainX509, hdr); wolfSSL_FreeX509(chainX509); } } /** * \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) { WOLFSSL *session; wget_tls_stats_data stats = { .alpn_protocol = NULL, .version = -1, .false_start = -1, .tfo = -1, .resumed = 0, .http_protocol = WGET_PROTOCOL_HTTP_1_1, .cert_chain_size = 0 }; int rc, ret = WGET_E_UNKNOWN; int sockfd, connect_timeout; const char *hostname; long long before_millisecs = 0; if (!tcp) return WGET_E_INVALID; if (!init) wget_ssl_init(); hostname = tcp->ssl_hostname; sockfd= tcp->sockfd; connect_timeout = tcp->connect_timeout; if ((session = wolfSSL_new(ssl_ctx)) == NULL) { error_printf(_("Failed to create WolfSSL session\n")); return -1; } // RFC 6066 SNI Server Name Indication if (hostname) wolfSSL_UseSNI(session, WOLFSSL_SNI_HOST_NAME, hostname, (unsigned short) strlen(hostname)); // if (tcp->tls_false_start) // info_printf(_("WolfSSL doesn't support TLS False Start\n")); if (config.alpn) { size_t len = strlen(config.alpn); char alpnbuf[256], *alpn; // wolfSSL_UseALPN() destroys the ALPN string (bad design pattern !) alpn = wget_strmemcpy_a(alpnbuf, sizeof(alpnbuf), config.alpn, strlen(config.alpn)); if (wolfSSL_UseALPN(session, alpn, (int) len, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH) == WOLFSSL_SUCCESS) { debug_printf("ALPN offering %s\n", config.alpn); } else debug_printf("WolfSSL: Failed to set ALPN: %s\n", config.alpn); if (alpn != alpnbuf) xfree(alpn); } // struct session_context *ctx = wget_calloc(1, sizeof(struct session_context)); // ctx->hostname = wget_strdup(hostname); tcp->ssl_session = session; // gnutls_session_set_ptr(session, ctx); wolfSSL_set_fd(session, FD_TO_SOCKET(sockfd)); /* make wolfSSL object nonblocking */ wolfSSL_set_using_nonblock(session, 1); if (tls_stats_callback) before_millisecs = wget_get_timemillis(); ret = do_handshake(session, sockfd, connect_timeout); if (tls_stats_callback) { long long after_millisecs = wget_get_timemillis(); stats.tls_secs = after_millisecs - before_millisecs; stats.tls_con = 1; stats.false_start = 0; // WolfSSL doesn't support False Start (https://www.wolfssl.com/is-tls-false-start-going-to-take-off-2/) } const char *name; int bits; WOLFSSL_CIPHER *cipher; WOLFSSL_X509 *peer = wolfSSL_get_peer_certificate(session); if (peer) { ShowX509(peer, "Peer's cert info"); wolfSSL_FreeX509(peer); } else debug_printf("Peer has no cert!\n"); ShowX509(wolfSSL_get_certificate(session), "our cert info:"); debug_printf("Peer verify result = %ld\n", wolfSSL_get_verify_result(session)); debug_printf("SSL version %s\n", wolfSSL_get_version(session)); cipher = wolfSSL_get_current_cipher(session); // printf("%s %s%s\n", words[1], (wolfSSL_isQSH(session)) ? "QSH:" : "", wolfSSL_CIPHER_get_name(cipher)); debug_printf("SSL cipher suite %s\n", wolfSSL_CIPHER_get_name(cipher)); if ((name = wolfSSL_get_curve_name(session))) debug_printf("SSL curve name %s\n", name); else if ((bits = wolfSSL_GetDhKey_Sz(session)) > 0) debug_printf("SSL DH size %d bits\n", bits); if (config.alpn) { char *protocol; uint16_t protocol_length; if (wolfSSL_ALPN_GetProtocol(session, &protocol, &protocol_length) != WOLFSSL_SUCCESS) debug_printf("WolfSSL: Failed to connect ALPN\n"); else { debug_printf("WolfSSL: Server accepted ALPN protocol '%.*s'\n", (int) protocol_length, protocol); if (tls_stats_callback) stats.alpn_protocol = wget_strmemdup(protocol, protocol_length); if (protocol_length == 2 && !memcmp(protocol, "h2", 2)) { tcp->protocol = WGET_PROTOCOL_HTTP_2_0; stats.http_protocol = WGET_PROTOCOL_HTTP_2_0; } } } if (ret == WGET_E_SUCCESS) { int resumed = wolfSSL_session_reused(session); WOLFSSL_X509_CHAIN *chain = (WOLFSSL_X509_CHAIN *) wolfSSL_get_peer_cert_chain(session); ShowX509Chain(chain, wolfSSL_get_chain_count(chain), "Certificate chain"); if (tls_stats_callback) { stats.resumed = resumed; stats.cert_chain_size = wolfSSL_get_chain_count(chain); const char *tlsver = wolfSSL_get_version(session); if (!strcmp(tlsver, "TLSv1.2")) stats.version = 4; else if (!strcmp(tlsver, "TLSv1.3")) stats.version = 5; else stats.version = 1; // SSLv3 } debug_printf("Handshake completed%s\n", resumed ? " (resumed session)" : ""); if (!resumed && config.tls_session_cache) { /* WOLFSSL_SESSION *session_data = wolfSSL_get_session(session); if (session_data) { int session_data_size = wolfSSL_get_session_cache_memsize(); char session_data_data[session_data_size]; if (wolfSSL_memsave_session_cache(session_data_data, session_data_size) == SSL_SUCCESS) { wget_tls_session_db_add(config.tls_session_cache, wget_tls_session_new(ctx->hostname, 18 * 3600, session_data.data, session_data.size)); // 18h valid } } */ /* gnutls_datum_t session_data; if ((rc = gnutls_session_get_data2(session, &session_data)) == GNUTLS_E_SUCCESS) { wget_tls_session_db_add(config.tls_session_cache, wget_tls_session_new(ctx->hostname, 18 * 3600, session_data.data, session_data.size)); // 18h valid gnutls_free(session_data.data); } else debug_printf("Failed to get session data: %s", gnutls_strerror(rc)); */ } } if ((rc = wolfSSL_connect(session)) != WOLFSSL_SUCCESS) { rc = wolfSSL_get_error(session, rc); error_printf(_("failed to connect TLS (%d): %s\n"), rc, wolfSSL_ERR_reason_error_string(rc)); long res = wolfSSL_get_verify_result(session); if (res >= 13 && res <= 29) return WGET_E_CERTIFICATE; else return WGET_E_CONNECT; } if (tls_stats_callback) { stats.hostname = hostname; tls_stats_callback(&stats, tls_stats_ctx); xfree(stats.alpn_protocol); } // tcp->hpkp = ctx->stats_hpkp; if (ret != WGET_E_SUCCESS) { if (ret == WGET_E_TIMEOUT) debug_printf("Handshake timed out\n"); // xfree(ctx->hostname); // xfree(ctx); wolfSSL_free(session); tcp->ssl_session = NULL; } return ret; } /** * \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) { if (session && *session) { WOLFSSL *s = *session; int ret; do { ret = wolfSSL_shutdown(s); ret = wolfSSL_get_error(s, ret); } while (ret == WOLFSSL_SHUTDOWN_NOT_DONE); if (ret < 0) debug_printf("TLS shutdown failed: %s\n", wolfSSL_ERR_reason_error_string(ret)); wolfSSL_free(s); *session = NULL; } } /** * \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 sockfd = SOCKET_TO_FD( wolfSSL_get_fd(session)); int rc; while ((rc = wolfSSL_read(session, buf, (int) count)) < 0) { rc = wolfSSL_get_error(session, rc); debug_printf("wolfSSL_read: (%d) (errno=%d) %s\n", rc, errno, wolfSSL_ERR_reason_error_string(rc)); if (rc == SSL_ERROR_WANT_READ) { if ((rc = wget_ready_2_read(sockfd, timeout)) <= 0) break; } else break; } return rc < 0 ? -1 : rc; /* for (;;) { int rc; if (gnutls_record_check_pending(session) <= 0 && (rc = wget_ready_2_read(sockfd, timeout)) <= 0) return rc; nbytes = gnutls_record_recv(session, buf, count); // If False Start + Session Resumption are enabled, we get the session data after the first read() struct session_context *ctx = gnutls_session_get_ptr(session); if (ctx && ctx->delayed_session_data) { gnutls_datum_t session_data; if ((rc = gnutls_session_get_data2(session, &session_data)) == GNUTLS_E_SUCCESS) { debug_printf("Got delayed session data\n"); ctx->delayed_session_data = 0; wget_tls_session_db_add(config.tls_session_cache, wget_tls_session_new(ctx->hostname, 18 * 3600, session_data.data, session_data.size)); // 18h valid gnutls_free(session_data.data); } else debug_printf("No delayed session data%s\n", gnutls_strerror(rc)); } if (nbytes == GNUTLS_E_REHANDSHAKE) { debug_printf("*** REHANDSHAKE while reading\n"); if ((nbytes = do_handshake(session, sockfd, timeout)) == 0) nbytes = GNUTLS_E_AGAIN; // restart reading } if (nbytes >= 0 || nbytes != GNUTLS_E_AGAIN) break; } return nbytes < -1 ? -1 : nbytes; */ } /** * \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 sockfd = SOCKET_TO_FD(wolfSSL_get_fd(session)); int rc; while ((rc = wolfSSL_write(session, buf, (int) count)) < 0) { rc = wolfSSL_get_error(session, rc); debug_printf("wolfSSL_write: (%d) (errno=%d) %s\n", rc, errno, wolfSSL_ERR_reason_error_string(rc)); if (rc == SSL_ERROR_WANT_WRITE) { if ((rc = wget_ready_2_write(sockfd, timeout)) <= 0) break; } else break; } return rc < 0 ? -1 : rc; /* for (;;) { ssize_t nbytes; int rc; if ((rc = wget_ready_2_write(sockfd, timeout)) <= 0) return rc; if ((nbytes = gnutls_record_send(session, buf, count)) >= 0) return nbytes; if (nbytes == GNUTLS_E_REHANDSHAKE) { debug_printf("*** REHANDSHAKE while writing\n"); if ((nbytes = do_handshake(session, sockfd, timeout)) == 0) continue; // restart writing } if (nbytes == GNUTLS_E_AGAIN) return 0; // indicate timeout return -1; } */ } /** * \param[in] fn A `wget_ssl_stats_callback_tls_t` 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_t` 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; } /** @} */