diff --git a/include/curl/curl.h b/include/curl/curl.h index f38bf97cba6e..db2c53cf89f5 100644 --- a/include/curl/curl.h +++ b/include/curl/curl.h @@ -788,6 +788,19 @@ typedef CURLcode (*curl_ssl_ctx_callback)(CURL *curl, /* easy handle */ mbedtls_ssl_config */ void *userptr); +#if UNITY_CERTVERIFY +/* Unity extension: CURLOPT_UNITY_CERTVERIFY_FUNCTION callback prototype. + Called with the DER-encoded leaf certificate of the peer once it is + available, and fully replaces the TLS backend's own peer verification. + Return CURLE_OK to accept the peer or CURLE_PEER_FAILED_VERIFICATION to + reject it. Unlike curl_ssl_ctx_callback this carries no backend-specific + types, so one implementation serves every backend. */ +typedef CURLcode (*curl_unity_certverify_callback)(CURL *curl, + const unsigned char *der, + size_t derlen, + void *userptr); +#endif /* UNITY_CERTVERIFY */ + #define CURLPROXY_HTTP 0L /* added in 7.10, new in 7.19.4 default is to use CONNECT HTTP/1.1 */ #define CURLPROXY_HTTP_1_0 1L /* force to use CONNECT HTTP/1.0 @@ -2259,6 +2272,20 @@ typedef enum { /* set TLS supported signature algorithms */ CURLOPT(CURLOPT_SSL_SIGNATURE_ALGORITHMS, CURLOPTTYPE_STRINGPOINT, 328), +#if UNITY_CERTVERIFY + /* Unity extensions. Numbered from 900 to stay clear of upstream option + numbers, so a curl update can never silently collide with these. */ + + /* callback that verifies the peer certificate, replacing the backend's own + verification entirely */ + CURLOPT(CURLOPT_UNITY_CERTVERIFY_FUNCTION, CURLOPTTYPE_FUNCTIONPOINT, 900), + + /* userdata passed to CURLOPT_UNITY_CERTVERIFY_FUNCTION. Also identifies the + verifier for connection reuse: connections are only shared between + transfers using the same callback and userdata. */ + CURLOPT(CURLOPT_UNITY_CERTVERIFY_DATA, CURLOPTTYPE_CBPOINT, 901), +#endif /* UNITY_CERTVERIFY */ + CURLOPT_LASTENTRY /* the last unused */ } CURLoption; diff --git a/lib/easyoptions.c b/lib/easyoptions.c index 4c0b4f0ce4ad..a559362c8e83 100644 --- a/lib/easyoptions.c +++ b/lib/easyoptions.c @@ -355,6 +355,11 @@ const struct curl_easyoption Curl_easyopts[] = { { "TRAILERFUNCTION", CURLOPT_TRAILERFUNCTION, CURLOT_FUNCTION, 0 }, { "TRANSFERTEXT", CURLOPT_TRANSFERTEXT, CURLOT_LONG, 0 }, { "TRANSFER_ENCODING", CURLOPT_TRANSFER_ENCODING, CURLOT_LONG, 0 }, +#if UNITY_CERTVERIFY + { "UNITY_CERTVERIFY_DATA", CURLOPT_UNITY_CERTVERIFY_DATA, CURLOT_CBPTR, 0 }, + { "UNITY_CERTVERIFY_FUNCTION", CURLOPT_UNITY_CERTVERIFY_FUNCTION, + CURLOT_FUNCTION, 0 }, +#endif /* UNITY_CERTVERIFY */ { "UNIX_SOCKET_PATH", CURLOPT_UNIX_SOCKET_PATH, CURLOT_STRING, 0 }, { "UNRESTRICTED_AUTH", CURLOPT_UNRESTRICTED_AUTH, CURLOT_LONG, 0 }, { "UPKEEP_INTERVAL_MS", CURLOPT_UPKEEP_INTERVAL_MS, CURLOT_LONG, 0 }, @@ -385,6 +390,10 @@ const struct curl_easyoption Curl_easyopts[] = { */ int Curl_easyopts_check(void) { +#if UNITY_CERTVERIFY + return (CURLOPT_LASTENTRY % 10000) != (901 + 1); +#else return (CURLOPT_LASTENTRY % 10000) != (328 + 1); +#endif /* UNITY_CERTVERIFY */ } #endif diff --git a/lib/setopt.c b/lib/setopt.c index b8a632748c7a..a23b5151a79f 100644 --- a/lib/setopt.c +++ b/lib/setopt.c @@ -2074,6 +2074,18 @@ static CURLcode setopt_cptr(struct Curl_easy *data, CURLoption option, */ s->ioctl_client = ptr; break; +#if UNITY_CERTVERIFY + case CURLOPT_UNITY_CERTVERIFY_DATA: + /* + * Unity: userdata for CURLOPT_UNITY_CERTVERIFY_FUNCTION. Might be NULL. + */ +#ifdef USE_SSL + s->ssl.primary.unity_certverify_userp = ptr; + break; +#else + return CURLE_NOT_BUILT_IN; +#endif +#endif /* UNITY_CERTVERIFY */ case CURLOPT_SSL_CTX_DATA: /* * Set an SSL_CTX callback parameter pointer @@ -2622,6 +2634,20 @@ static CURLcode setopt_func(struct Curl_easy *data, CURLoption option, #endif return CURLE_NOT_BUILT_IN; +#if UNITY_CERTVERIFY + case CURLOPT_UNITY_CERTVERIFY_FUNCTION: + /* + * Unity: set a callback that verifies the peer certificate. + */ +#ifdef USE_SSL + s->ssl.primary.unity_certverify = + va_arg(param, curl_unity_certverify_callback); + break; +#else + return CURLE_NOT_BUILT_IN; +#endif +#endif /* UNITY_CERTVERIFY */ + case CURLOPT_SOCKOPTFUNCTION: /* * socket callback function: called after socket() but before connect() diff --git a/lib/urldata.h b/lib/urldata.h index f35bfee053d0..83d21d954984 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -156,6 +156,14 @@ struct ssl_primary_config { char *password; /* TLS password (for, e.g., SRP) */ #endif char *curves; /* list of curves to use */ +#if UNITY_CERTVERIFY + /* Unity: peer verification callback and its userdata. These live in the + primary config rather than in ssl_config_data so that they take part in + the connection reuse check, i.e. a connection verified by one callback is + never reused by a transfer carrying a different one. */ + curl_unity_certverify_callback unity_certverify; + void *unity_certverify_userp; +#endif /* UNITY_CERTVERIFY */ uint32_t version_max; /* max supported version the client wants to use */ uint8_t ssl_options; /* the CURLOPT_SSL_OPTIONS bitmask */ uint8_t version; /* what version the client wants to use */ diff --git a/lib/vtls/openssl.c b/lib/vtls/openssl.c index 4629ca444435..f3dc8c93bfaf 100644 --- a/lib/vtls/openssl.c +++ b/lib/vtls/openssl.c @@ -4771,6 +4771,22 @@ CURLcode Curl_ossl_check_peer_cert(struct Curl_cfilter *cf, infof_certstack(data, octx->ssl); #endif +#if UNITY_CERTVERIFY + if(conn_config->unity_certverify) { + unsigned char *der = NULL; + int derlen = i2d_X509(server_cert, &der); + + if(derlen <= 0) { + failf(data, "SSL: could not DER encode peer certificate"); + result = CURLE_PEER_FAILED_VERIFICATION; + goto out; + } + result = Curl_unity_certverify(cf, data, der, (size_t)derlen); + OPENSSL_free(der); + goto out; + } +#endif /* UNITY_CERTVERIFY */ + if(conn_config->verifyhost) { result = ossl_verifyhost(data, conn, peer, server_cert); if(result) diff --git a/lib/vtls/schannel.c b/lib/vtls/schannel.c index fb5ff0e9d8e6..e1c8251af31c 100644 --- a/lib/vtls/schannel.c +++ b/lib/vtls/schannel.c @@ -865,6 +865,14 @@ static CURLcode schannel_connect_step1(struct Curl_cfilter *cf, else backend->use_manual_cred_validation = FALSE; +#if UNITY_CERTVERIFY + /* Schannel must not reject the peer during the handshake when a verification + callback is installed; the callback has the final say. Manual validation + moves the decision into Curl_verify_certificate, where it is consulted. */ + if(conn_config->unity_certverify) + backend->use_manual_cred_validation = TRUE; +#endif /* UNITY_CERTVERIFY */ + backend->cred = NULL; /* check for an existing reusable credential handle */ @@ -1469,7 +1477,14 @@ static CURLcode schannel_connect_step2(struct Curl_cfilter *cf, } } +#if UNITY_CERTVERIFY + /* A verification callback needs the peer certificate whether or not + verifypeer is set, and Curl_verify_certificate hands it over. */ + if(backend->use_manual_cred_validation && + (conn_config->verifypeer || conn_config->unity_certverify)) { +#else if(conn_config->verifypeer && backend->use_manual_cred_validation) { +#endif /* UNITY_CERTVERIFY */ /* Certificate verification also verifies the hostname if verifyhost */ return Curl_verify_certificate(cf, data); } diff --git a/lib/vtls/schannel_verify.c b/lib/vtls/schannel_verify.c index 9be6fe311ccd..fc797349b0eb 100644 --- a/lib/vtls/schannel_verify.c +++ b/lib/vtls/schannel_verify.c @@ -684,6 +684,16 @@ CURLcode Curl_verify_certificate(struct Curl_cfilter *cf, result = CURLE_PEER_FAILED_VERIFICATION; } +#if UNITY_CERTVERIFY + if(result == CURLE_OK && conn_config->unity_certverify) { + result = Curl_unity_certverify(cf, data, + pCertContextServer->pbCertEncoded, + pCertContextServer->cbCertEncoded); + CertFreeCertificateContext(pCertContextServer); + return result; + } +#endif /* UNITY_CERTVERIFY */ + if(result == CURLE_OK && (conn_config->CAfile || conn_config->ca_info_blob) && BACKEND->use_manual_cred_validation) { diff --git a/lib/vtls/unitytls.c b/lib/vtls/unitytls.c index bbbdec49737e..a6eed64e92ae 100644 --- a/lib/vtls/unitytls.c +++ b/lib/vtls/unitytls.c @@ -290,6 +290,42 @@ static unitytls_x509verify_result unitytls_on_verify(void* userData, unitytls_x5 const char* const hostname = connssl->peer.hostname; unitytls_x509verify_result verify_result = UNITYTLS_X509VERIFY_SUCCESS; +#if UNITY_CERTVERIFY + if(conn_config->unity_certverify) { + const unitytls_x509verify_result fatal = + (unitytls_x509verify_result)UNITYTLS_X509VERIFY_FATAL_ERROR; + unitytls_x509_ref leaf; + size_t derlen; + UInt8* der; + CURLcode result; + + leaf = unitytls->unitytls_x509list_get_x509(chain, 0, errorState); + if(errorState->code != UNITYTLS_SUCCESS) + return fatal; + + derlen = unitytls->unitytls_x509_export_der(leaf, NULL, 0, errorState); + if(errorState->code != UNITYTLS_SUCCESS || !derlen) + return fatal; + + der = curlx_malloc(derlen); + if(!der) + return fatal; + + unitytls->unitytls_x509_export_der(leaf, der, derlen, errorState); + if(errorState->code != UNITYTLS_SUCCESS) { + curlx_free(der); + return fatal; + } + + result = Curl_unity_certverify(cf, CF_DATA_CURRENT(cf), der, derlen); + curlx_free(der); + + if(result) + return (unitytls_x509verify_result)UNITYTLS_X509VERIFY_FLAG_NOT_TRUSTED; + return (unitytls_x509verify_result)UNITYTLS_X509VERIFY_SUCCESS; + } +#endif /* UNITY_CERTVERIFY */ + /* According to documentation the options verifypeer and verifyhost are independent of each other! */ /* UnityTls however, verifies both the certificate as well as the hostname in the same call. */ if(verifypeer || verifyhost) { diff --git a/lib/vtls/vtls.c b/lib/vtls/vtls.c index a3c6892191f4..416fe9c74cde 100644 --- a/lib/vtls/vtls.c +++ b/lib/vtls/vtls.c @@ -61,6 +61,9 @@ #include "vtls/unitytls.h" /* UnityTls version */ #include "slist.h" +#if UNITY_CERTVERIFY +#include "multiif.h" +#endif /* UNITY_CERTVERIFY */ #include "curl_trc.h" #include "strcase.h" #include "url.h" @@ -203,6 +206,10 @@ static bool match_ssl_primary_config(struct Curl_easy *data, (c1->verifypeer == c2->verifypeer) && (c1->verifyhost == c2->verifyhost) && (c1->verifystatus == c2->verifystatus) && +#if UNITY_CERTVERIFY + (c1->unity_certverify == c2->unity_certverify) && + (c1->unity_certverify_userp == c2->unity_certverify_userp) && +#endif /* UNITY_CERTVERIFY */ blobcmp(c1->cert_blob, c2->cert_blob) && blobcmp(c1->ca_info_blob, c2->ca_info_blob) && blobcmp(c1->issuercert_blob, c2->issuercert_blob) && @@ -225,6 +232,38 @@ static bool match_ssl_primary_config(struct Curl_easy *data, return FALSE; } +#if UNITY_CERTVERIFY +CURLcode Curl_unity_certverify(struct Curl_cfilter *cf, + struct Curl_easy *data, + const unsigned char *der, + size_t derlen) +{ + struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf); + CURLcode result; + + DEBUGASSERT(conn_config->unity_certverify); + + /* Fail closed: without a certificate the callback cannot make a decision. */ + if(!der || !derlen) { + failf(data, "TLS: no peer certificate to verify"); + return CURLE_PEER_FAILED_VERIFICATION; + } + + Curl_set_in_callback(data, TRUE); + result = conn_config->unity_certverify(data, der, derlen, + conn_config->unity_certverify_userp); + Curl_set_in_callback(data, FALSE); + + if(result) { + failf(data, "TLS: peer certificate rejected by verification callback"); + return CURLE_PEER_FAILED_VERIFICATION; + } + + infof(data, "TLS: peer certificate accepted by verification callback"); + return CURLE_OK; +} +#endif /* UNITY_CERTVERIFY */ + bool Curl_ssl_conn_config_match(struct Curl_easy *data, struct connectdata *candidate, bool proxy) @@ -250,6 +289,10 @@ static bool clone_ssl_primary_config(struct ssl_primary_config *source, dest->verifystatus = source->verifystatus; dest->cache_session = source->cache_session; dest->ssl_options = source->ssl_options; +#if UNITY_CERTVERIFY + dest->unity_certverify = source->unity_certverify; + dest->unity_certverify_userp = source->unity_certverify_userp; +#endif /* UNITY_CERTVERIFY */ CLONE_BLOB(cert_blob); CLONE_BLOB(ca_info_blob); diff --git a/lib/vtls/vtls.h b/lib/vtls/vtls.h index 6db67cf7482e..5e415736b373 100644 --- a/lib/vtls/vtls.h +++ b/lib/vtls/vtls.h @@ -144,6 +144,19 @@ bool Curl_ssl_conn_config_match(struct Curl_easy *data, * `verifyhost` and `verifystatus`. */ void Curl_ssl_conn_config_update(struct Curl_easy *data, bool for_proxy); +#if UNITY_CERTVERIFY +/** + * Unity: hand the DER-encoded peer certificate to the + * CURLOPT_UNITY_CERTVERIFY_FUNCTION callback and translate its answer into a + * CURLcode. Only call this when `unity_certverify` is set on the connection's + * primary SSL config; it then replaces the backend's own peer verification. + */ +CURLcode Curl_unity_certverify(struct Curl_cfilter *cf, + struct Curl_easy *data, + const unsigned char *der, + size_t derlen); +#endif /* UNITY_CERTVERIFY */ + /** * Init SSL peer information for filter. Can be called repeatedly. */ diff --git a/lib/vtls/vtls_scache.c b/lib/vtls/vtls_scache.c index 59fa256bc31d..52c83698f9df 100644 --- a/lib/vtls/vtls_scache.c +++ b/lib/vtls/vtls_scache.c @@ -186,6 +186,19 @@ CURLcode Curl_ssl_peer_key_make(struct Curl_cfilter *cf, if(r) goto out; } +#if UNITY_CERTVERIFY + /* A verification callback replaces the backend's own peer verification, and + on Schannel it also changes the credential flags cached under this key + (SCH_CRED_MANUAL_CRED_VALIDATION). Without this, a credential established + under a callback could be picked up by a transfer that has none, which + would then neither validate automatically nor call a callback. */ + if(ssl->unity_certverify) { + r = curlx_dyn_addf(&buf, ":UNITY-CERTVERIFY-%p", + ssl->unity_certverify_userp); + if(r) + goto out; + } +#endif /* UNITY_CERTVERIFY */ if(!ssl->verifypeer || !ssl->verifyhost) { if(cf->conn->bits.conn_to_host) { r = curlx_dyn_addf(&buf, ":CHOST-%s", cf->conn->conn_to_host.name);