mirror of
https://github.com/curl/curl.git
synced 2026-06-11 11:54:14 +03:00
openssl: enable builds for *both* engines and providers
OpenSSL3 can in fact have both enabled at once. Load the provider and key/cert appropriately. When loading a provider, the user can now also set an associated "property string". Work on this was sponsored by Valantic. Closes #17165
This commit is contained in:
parent
e0ebc3ff13
commit
f2ce6c46b9
3 changed files with 151 additions and 83 deletions
|
|
@ -17,7 +17,7 @@ Added-in: 7.9.3
|
||||||
|
|
||||||
# NAME
|
# NAME
|
||||||
|
|
||||||
CURLOPT_SSLENGINE - SSL engine identifier
|
CURLOPT_SSLENGINE - Set SSL engine or provider
|
||||||
|
|
||||||
# SYNOPSIS
|
# SYNOPSIS
|
||||||
|
|
||||||
|
|
@ -30,11 +30,16 @@ CURLcode curl_easy_setopt(CURL *handle, CURLOPT_SSLENGINE, char *id);
|
||||||
# DESCRIPTION
|
# DESCRIPTION
|
||||||
|
|
||||||
Pass a pointer to a null-terminated string as parameter. It is used as the
|
Pass a pointer to a null-terminated string as parameter. It is used as the
|
||||||
identifier for the crypto engine you want to use for your private key.
|
identifier for the *engine* or *provider* you want to use for your private
|
||||||
|
key. OpenSSL 1 had engines, OpenSSL 3 has providers.
|
||||||
|
|
||||||
The application does not have to keep the string around after setting this
|
The application does not have to keep the string around after setting this
|
||||||
option.
|
option.
|
||||||
|
|
||||||
|
When asking libcurl to use a provider, the application can also optionally
|
||||||
|
provide a *property*, a set of name value pairs. Such a property can be
|
||||||
|
specified separated from the name with a colon (`:`).
|
||||||
|
|
||||||
Using this option multiple times makes the last set string override the
|
Using this option multiple times makes the last set string override the
|
||||||
previous ones. Set it to NULL to disable its use again.
|
previous ones. Set it to NULL to disable its use again.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1196,10 +1196,13 @@ struct UrlState {
|
||||||
#if defined(USE_OPENSSL)
|
#if defined(USE_OPENSSL)
|
||||||
/* void instead of ENGINE to avoid bleeding OpenSSL into this header */
|
/* void instead of ENGINE to avoid bleeding OpenSSL into this header */
|
||||||
void *engine;
|
void *engine;
|
||||||
/* this is just a flag -- we do not need to reference the provider in any
|
/* void instead of OSSL_PROVIDER */
|
||||||
* way as OpenSSL takes care of that */
|
void *provider;
|
||||||
BIT(provider);
|
void *baseprov;
|
||||||
BIT(provider_failed);
|
void *libctx;
|
||||||
|
char *propq; /* for a provider */
|
||||||
|
|
||||||
|
BIT(provider_loaded);
|
||||||
#endif /* USE_OPENSSL */
|
#endif /* USE_OPENSSL */
|
||||||
struct curltime expiretime; /* set this with Curl_expire() only */
|
struct curltime expiretime; /* set this with Curl_expire() only */
|
||||||
struct Curl_tree timenode; /* for the splay stuff */
|
struct Curl_tree timenode; /* for the splay stuff */
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@
|
||||||
#include "../strcase.h"
|
#include "../strcase.h"
|
||||||
#include "hostcheck.h"
|
#include "hostcheck.h"
|
||||||
#include "../multiif.h"
|
#include "../multiif.h"
|
||||||
|
#include "../strparse.h"
|
||||||
#include "../strdup.h"
|
#include "../strdup.h"
|
||||||
#include "../strerror.h"
|
#include "../strerror.h"
|
||||||
#include "../curl_printf.h"
|
#include "../curl_printf.h"
|
||||||
|
|
@ -117,6 +118,8 @@
|
||||||
#include <openssl/store.h>
|
#include <openssl/store.h>
|
||||||
/* this is used in the following conditions to make them easier to read */
|
/* this is used in the following conditions to make them easier to read */
|
||||||
#define OPENSSL_HAS_PROVIDERS
|
#define OPENSSL_HAS_PROVIDERS
|
||||||
|
|
||||||
|
static void ossl_provider_cleanup(struct Curl_easy *data);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "../warnless.h"
|
#include "../warnless.h"
|
||||||
|
|
@ -1100,7 +1103,7 @@ static bool is_pkcs11_uri(const char *string)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static CURLcode ossl_set_engine(struct Curl_easy *data, const char *engine);
|
static CURLcode ossl_set_engine(struct Curl_easy *data, const char *engine);
|
||||||
#if !defined(USE_OPENSSL_ENGINE) && defined(OPENSSL_HAS_PROVIDERS)
|
#if defined(OPENSSL_HAS_PROVIDERS)
|
||||||
static CURLcode ossl_set_provider(struct Curl_easy *data,
|
static CURLcode ossl_set_provider(struct Curl_easy *data,
|
||||||
const char *provider);
|
const char *provider);
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -1353,7 +1356,8 @@ int cert_stuff(struct Curl_easy *data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
#elif defined(OPENSSL_HAS_PROVIDERS)
|
#endif
|
||||||
|
#if defined(OPENSSL_HAS_PROVIDERS)
|
||||||
/* fall through to compatible provider */
|
/* fall through to compatible provider */
|
||||||
case SSL_FILETYPE_PROVIDER:
|
case SSL_FILETYPE_PROVIDER:
|
||||||
{
|
{
|
||||||
|
|
@ -1369,10 +1373,11 @@ int cert_stuff(struct Curl_easy *data,
|
||||||
|
|
||||||
if(data->state.provider) {
|
if(data->state.provider) {
|
||||||
/* Load the certificate from the provider */
|
/* Load the certificate from the provider */
|
||||||
OSSL_STORE_CTX *store = NULL;
|
|
||||||
OSSL_STORE_INFO *info = NULL;
|
OSSL_STORE_INFO *info = NULL;
|
||||||
X509 *cert = NULL;
|
X509 *cert = NULL;
|
||||||
store = OSSL_STORE_open(cert_file, NULL, NULL, NULL, NULL);
|
OSSL_STORE_CTX *store =
|
||||||
|
OSSL_STORE_open_ex(cert_file, data->state.libctx,
|
||||||
|
NULL, NULL, NULL, NULL, NULL, NULL);
|
||||||
if(!store) {
|
if(!store) {
|
||||||
failf(data, "Failed to open OpenSSL store: %s",
|
failf(data, "Failed to open OpenSSL store: %s",
|
||||||
ossl_strerror(ERR_get_error(), error_buffer,
|
ossl_strerror(ERR_get_error(), error_buffer,
|
||||||
|
|
@ -1385,22 +1390,13 @@ int cert_stuff(struct Curl_easy *data,
|
||||||
sizeof(error_buffer)));
|
sizeof(error_buffer)));
|
||||||
}
|
}
|
||||||
|
|
||||||
for(info = OSSL_STORE_load(store);
|
info = OSSL_STORE_load(store);
|
||||||
info != NULL;
|
if(info) {
|
||||||
info = OSSL_STORE_load(store)) {
|
|
||||||
int ossl_type = OSSL_STORE_INFO_get_type(info);
|
int ossl_type = OSSL_STORE_INFO_get_type(info);
|
||||||
|
|
||||||
if(ossl_type == OSSL_STORE_INFO_CERT) {
|
if(ossl_type == OSSL_STORE_INFO_CERT)
|
||||||
cert = OSSL_STORE_INFO_get1_CERT(info);
|
cert = OSSL_STORE_INFO_get1_CERT(info);
|
||||||
}
|
|
||||||
else {
|
|
||||||
failf(data, "Ignoring object not matching our type: %d",
|
|
||||||
ossl_type);
|
|
||||||
OSSL_STORE_INFO_free(info);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
OSSL_STORE_INFO_free(info);
|
OSSL_STORE_INFO_free(info);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
OSSL_STORE_close(store);
|
OSSL_STORE_close(store);
|
||||||
if(!cert) {
|
if(!cert) {
|
||||||
|
|
@ -1424,9 +1420,6 @@ int cert_stuff(struct Curl_easy *data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
#else
|
|
||||||
failf(data, "file type ENG nor PROV for certificate not implemented");
|
|
||||||
return 0;
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
case SSL_FILETYPE_PKCS12:
|
case SSL_FILETYPE_PKCS12:
|
||||||
|
|
@ -1616,7 +1609,8 @@ fail:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
#elif defined(OPENSSL_HAS_PROVIDERS)
|
#endif
|
||||||
|
#if defined(OPENSSL_HAS_PROVIDERS)
|
||||||
/* fall through to compatible provider */
|
/* fall through to compatible provider */
|
||||||
case SSL_FILETYPE_PROVIDER:
|
case SSL_FILETYPE_PROVIDER:
|
||||||
{
|
{
|
||||||
|
|
@ -1647,7 +1641,9 @@ fail:
|
||||||
UI_method_set_reader(ui_method, ssl_ui_reader);
|
UI_method_set_reader(ui_method, ssl_ui_reader);
|
||||||
UI_method_set_writer(ui_method, ssl_ui_writer);
|
UI_method_set_writer(ui_method, ssl_ui_writer);
|
||||||
|
|
||||||
store = OSSL_STORE_open(key_file, ui_method, NULL, NULL, NULL);
|
store = OSSL_STORE_open_ex(key_file, data->state.libctx,
|
||||||
|
data->state.propq, ui_method, NULL, NULL,
|
||||||
|
NULL, NULL);
|
||||||
if(!store) {
|
if(!store) {
|
||||||
failf(data, "Failed to open OpenSSL store: %s",
|
failf(data, "Failed to open OpenSSL store: %s",
|
||||||
ossl_strerror(ERR_get_error(), error_buffer,
|
ossl_strerror(ERR_get_error(), error_buffer,
|
||||||
|
|
@ -1660,22 +1656,13 @@ fail:
|
||||||
sizeof(error_buffer)));
|
sizeof(error_buffer)));
|
||||||
}
|
}
|
||||||
|
|
||||||
for(info = OSSL_STORE_load(store);
|
info = OSSL_STORE_load(store);
|
||||||
info != NULL;
|
if(info) {
|
||||||
info = OSSL_STORE_load(store)) {
|
|
||||||
int ossl_type = OSSL_STORE_INFO_get_type(info);
|
int ossl_type = OSSL_STORE_INFO_get_type(info);
|
||||||
|
|
||||||
if(ossl_type == OSSL_STORE_INFO_PKEY) {
|
if(ossl_type == OSSL_STORE_INFO_PKEY)
|
||||||
priv_key = OSSL_STORE_INFO_get1_PKEY(info);
|
priv_key = OSSL_STORE_INFO_get1_PKEY(info);
|
||||||
}
|
|
||||||
else {
|
|
||||||
failf(data, "Ignoring object not matching our type: %d",
|
|
||||||
ossl_type);
|
|
||||||
OSSL_STORE_INFO_free(info);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
OSSL_STORE_INFO_free(info);
|
OSSL_STORE_INFO_free(info);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
OSSL_STORE_close(store);
|
OSSL_STORE_close(store);
|
||||||
UI_destroy_method(ui_method);
|
UI_destroy_method(ui_method);
|
||||||
|
|
@ -1701,9 +1688,6 @@ fail:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
#else
|
|
||||||
failf(data, "file type ENG nor PROV for private key not implemented");
|
|
||||||
return 0;
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
case SSL_FILETYPE_PKCS12:
|
case SSL_FILETYPE_PKCS12:
|
||||||
|
|
@ -1878,36 +1862,39 @@ static void ossl_cleanup(void)
|
||||||
Curl_tls_keylog_close();
|
Curl_tls_keylog_close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Selects an OpenSSL crypto engine
|
/* Selects an OpenSSL crypto engine or provider.
|
||||||
*/
|
*/
|
||||||
static CURLcode ossl_set_engine(struct Curl_easy *data, const char *engine)
|
static CURLcode ossl_set_engine(struct Curl_easy *data, const char *name)
|
||||||
{
|
{
|
||||||
#ifdef USE_OPENSSL_ENGINE
|
#ifdef USE_OPENSSL_ENGINE
|
||||||
ENGINE *e = ENGINE_by_id(engine);
|
CURLcode result = CURLE_SSL_ENGINE_NOTFOUND;
|
||||||
|
ENGINE *e = ENGINE_by_id(name);
|
||||||
|
|
||||||
if(!e) {
|
if(e) {
|
||||||
failf(data, "SSL Engine '%s' not found", engine);
|
|
||||||
return CURLE_SSL_ENGINE_NOTFOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(data->state.engine) {
|
if(data->state.engine) {
|
||||||
ENGINE_finish(data->state.engine);
|
ENGINE_finish(data->state.engine);
|
||||||
ENGINE_free(data->state.engine);
|
ENGINE_free(data->state.engine);
|
||||||
data->state.engine = NULL;
|
data->state.engine = NULL;
|
||||||
}
|
}
|
||||||
if(!ENGINE_init(e)) {
|
if(!ENGINE_init(e)) {
|
||||||
char buf[256];
|
char buf[256];
|
||||||
|
|
||||||
ENGINE_free(e);
|
ENGINE_free(e);
|
||||||
failf(data, "Failed to initialise SSL Engine '%s': %s",
|
failf(data, "Failed to initialise SSL Engine '%s': %s",
|
||||||
engine, ossl_strerror(ERR_get_error(), buf, sizeof(buf)));
|
name, ossl_strerror(ERR_get_error(), buf, sizeof(buf)));
|
||||||
return CURLE_SSL_ENGINE_INITFAILED;
|
result = CURLE_SSL_ENGINE_INITFAILED;
|
||||||
|
e = NULL;
|
||||||
|
}
|
||||||
|
data->state.engine = e;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
data->state.engine = e;
|
#endif
|
||||||
return CURLE_OK;
|
#ifdef OPENSSL_HAS_PROVIDERS
|
||||||
|
return ossl_set_provider(data, name);
|
||||||
#else
|
#else
|
||||||
(void)engine;
|
(void)name;
|
||||||
failf(data, "SSL Engine not supported");
|
failf(data, "OpenSSL engine not found");
|
||||||
return CURLE_SSL_ENGINE_NOTFOUND;
|
return CURLE_SSL_ENGINE_NOTFOUND;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
@ -1956,33 +1943,97 @@ static struct curl_slist *ossl_engines_list(struct Curl_easy *data)
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !defined(USE_OPENSSL_ENGINE) && defined(OPENSSL_HAS_PROVIDERS)
|
#if defined(OPENSSL_HAS_PROVIDERS)
|
||||||
/* Selects an OpenSSL crypto provider
|
|
||||||
*/
|
|
||||||
static CURLcode ossl_set_provider(struct Curl_easy *data, const char *provider)
|
|
||||||
{
|
|
||||||
OSSL_PROVIDER *pkcs11_provider = NULL;
|
|
||||||
char error_buffer[256];
|
|
||||||
|
|
||||||
if(OSSL_PROVIDER_available(NULL, provider)) {
|
static void ossl_provider_cleanup(struct Curl_easy *data)
|
||||||
/* already loaded through the configuration - no action needed */
|
{
|
||||||
data->state.provider = TRUE;
|
OSSL_LIB_CTX_free(data->state.libctx);
|
||||||
|
data->state.libctx = NULL;
|
||||||
|
Curl_safefree(data->state.propq);
|
||||||
|
if(data->state.baseprov) {
|
||||||
|
OSSL_PROVIDER_unload(data->state.baseprov);
|
||||||
|
data->state.baseprov = NULL;
|
||||||
|
}
|
||||||
|
if(data->state.provider) {
|
||||||
|
OSSL_PROVIDER_unload(data->state.provider);
|
||||||
|
data->state.provider = NULL;
|
||||||
|
}
|
||||||
|
data->state.provider_loaded = FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define MAX_PROVIDER_LEN 128 /* reasonable */
|
||||||
|
|
||||||
|
/* Selects an OpenSSL crypto provider.
|
||||||
|
*
|
||||||
|
* A provider might need an associated property, a string passed on to
|
||||||
|
* OpenSSL. Specify this as [PROVIDER][:PROPERTY]: separate the name and the
|
||||||
|
* property with a colon. No colon means no property is set.
|
||||||
|
*
|
||||||
|
* An example provider + property looks like "tpm2:?provider=tpm2".
|
||||||
|
*/
|
||||||
|
static CURLcode ossl_set_provider(struct Curl_easy *data, const char *iname)
|
||||||
|
{
|
||||||
|
char name[MAX_PROVIDER_LEN + 1];
|
||||||
|
struct Curl_str prov;
|
||||||
|
const char *propq = NULL;
|
||||||
|
|
||||||
|
if(!iname) {
|
||||||
|
/* clear and cleanup provider use */
|
||||||
|
ossl_provider_cleanup(data);
|
||||||
return CURLE_OK;
|
return CURLE_OK;
|
||||||
}
|
}
|
||||||
if(data->state.provider_failed) {
|
if(Curl_str_until(&iname, &prov, MAX_PROVIDER_LEN, ':'))
|
||||||
return CURLE_SSL_ENGINE_NOTFOUND;
|
return CURLE_BAD_FUNCTION_ARGUMENT;
|
||||||
|
|
||||||
|
if(!Curl_str_single(&iname, ':'))
|
||||||
|
/* there was a colon, get the propq until the end of string */
|
||||||
|
propq = iname;
|
||||||
|
|
||||||
|
/* we need the name in a buffer, null-terminated */
|
||||||
|
memcpy(name, Curl_str(&prov), Curl_strlen(&prov));
|
||||||
|
name[Curl_strlen(&prov)] = 0;
|
||||||
|
|
||||||
|
if(!data->state.libctx) {
|
||||||
|
OSSL_LIB_CTX *libctx = OSSL_LIB_CTX_new();
|
||||||
|
if(!libctx)
|
||||||
|
return CURLE_OUT_OF_MEMORY;
|
||||||
|
if(propq) {
|
||||||
|
data->state.propq = strdup(propq);
|
||||||
|
if(!data->state.propq) {
|
||||||
|
OSSL_LIB_CTX_free(libctx);
|
||||||
|
return CURLE_OUT_OF_MEMORY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data->state.libctx = libctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
pkcs11_provider = OSSL_PROVIDER_try_load(NULL, provider, 1);
|
if(OSSL_PROVIDER_available(data->state.libctx, name)) {
|
||||||
if(!pkcs11_provider) {
|
/* already loaded through the configuration - no action needed */
|
||||||
|
data->state.provider_loaded = TRUE;
|
||||||
|
return CURLE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
data->state.provider =
|
||||||
|
OSSL_PROVIDER_try_load(data->state.libctx, name, 1);
|
||||||
|
if(!data->state.provider) {
|
||||||
|
char error_buffer[256];
|
||||||
failf(data, "Failed to initialize provider: %s",
|
failf(data, "Failed to initialize provider: %s",
|
||||||
ossl_strerror(ERR_get_error(), error_buffer,
|
ossl_strerror(ERR_get_error(), error_buffer,
|
||||||
sizeof(error_buffer)));
|
sizeof(error_buffer)));
|
||||||
/* Do not attempt to load it again */
|
ossl_provider_cleanup(data);
|
||||||
data->state.provider_failed = TRUE;
|
|
||||||
return CURLE_SSL_ENGINE_NOTFOUND;
|
return CURLE_SSL_ENGINE_NOTFOUND;
|
||||||
}
|
}
|
||||||
data->state.provider = TRUE;
|
|
||||||
|
/* load the base provider as well */
|
||||||
|
data->state.baseprov =
|
||||||
|
OSSL_PROVIDER_try_load(data->state.libctx, "base", 1);
|
||||||
|
if(!data->state.baseprov) {
|
||||||
|
ossl_provider_cleanup(data);
|
||||||
|
failf(data, "Failed to load base");
|
||||||
|
return CURLE_SSL_ENGINE_NOTFOUND;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
data->state.provider_loaded = TRUE;
|
||||||
return CURLE_OK;
|
return CURLE_OK;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -2142,6 +2193,9 @@ static void ossl_close_all(struct Curl_easy *data)
|
||||||
#else
|
#else
|
||||||
(void)data;
|
(void)data;
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef OPENSSL_HAS_PROVIDERS
|
||||||
|
ossl_provider_cleanup(data);
|
||||||
|
#endif
|
||||||
#ifndef HAVE_ERR_REMOVE_THREAD_STATE_DEPRECATED
|
#ifndef HAVE_ERR_REMOVE_THREAD_STATE_DEPRECATED
|
||||||
/* OpenSSL 1.0.1 and 1.0.2 build an error queue that is stored per-thread
|
/* OpenSSL 1.0.1 and 1.0.2 build an error queue that is stored per-thread
|
||||||
so we need to clean it here in case the thread will be killed. All OpenSSL
|
so we need to clean it here in case the thread will be killed. All OpenSSL
|
||||||
|
|
@ -3634,7 +3688,13 @@ CURLcode Curl_ossl_ctx_init(struct ossl_ctx *octx,
|
||||||
|
|
||||||
|
|
||||||
DEBUGASSERT(!octx->ssl_ctx);
|
DEBUGASSERT(!octx->ssl_ctx);
|
||||||
octx->ssl_ctx = SSL_CTX_new(req_method);
|
octx->ssl_ctx =
|
||||||
|
#ifdef OPENSSL_HAS_PROVIDERS
|
||||||
|
data->state.libctx ?
|
||||||
|
SSL_CTX_new_ex(data->state.libctx, data->state.propq,
|
||||||
|
req_method):
|
||||||
|
#endif
|
||||||
|
SSL_CTX_new(req_method);
|
||||||
|
|
||||||
if(!octx->ssl_ctx) {
|
if(!octx->ssl_ctx) {
|
||||||
failf(data, "SSL: could not create a context: %s",
|
failf(data, "SSL: could not create a context: %s",
|
||||||
|
|
@ -5485,7 +5545,7 @@ const struct Curl_ssl Curl_ssl_openssl = {
|
||||||
ossl_get_internals, /* get_internals */
|
ossl_get_internals, /* get_internals */
|
||||||
ossl_close, /* close_one */
|
ossl_close, /* close_one */
|
||||||
ossl_close_all, /* close_all */
|
ossl_close_all, /* close_all */
|
||||||
ossl_set_engine, /* set_engine */
|
ossl_set_engine, /* set_engine or provider */
|
||||||
ossl_set_engine_default, /* set_engine_default */
|
ossl_set_engine_default, /* set_engine_default */
|
||||||
ossl_engines_list, /* engines_list */
|
ossl_engines_list, /* engines_list */
|
||||||
NULL, /* false_start */
|
NULL, /* false_start */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue