From bddb80a192d7a175c01f244f54318b2ec32efbab Mon Sep 17 00:00:00 2001 From: Steffen Jaeckel Date: Thu, 3 Feb 2022 09:11:49 +0100 Subject: [PATCH] add support for a password callback In order to be able to load password-protected key files a password callback was added. This also adds support for PKCS#12 containers instead of certificate+key. Signed-off-by: Steffen Jaeckel --- Makefile.am | 2 + examples/complex.c | 55 +++++++++++++++++- src/auth.c | 2 +- src/common.h | 2 + src/conn.c | 44 +++++++++++--- src/tls_gnutls.c | 103 ++++++++++++++++++++++++++++++-- src/tls_openssl.c | 126 +++++++++++++++++++++++++++++++++++++--- strophe.h | 36 ++++++++++++ tests/cert.pfx | Bin 0 -> 1246 bytes tests/key_encrypted.pem | 8 +++ tests/test.h | 22 +++---- tests/test_xmppaddr.c | 67 +++++++++++++-------- 12 files changed, 410 insertions(+), 57 deletions(-) create mode 100644 tests/cert.pfx create mode 100644 tests/key_encrypted.pem diff --git a/Makefile.am b/Makefile.am index 761df66..0b7a060 100644 --- a/Makefile.am +++ b/Makefile.am @@ -114,7 +114,9 @@ EXTRA_DIST = \ jni/Application.mk \ src/tls_schannel.c \ tests/cert.pem \ + tests/cert.pfx \ tests/key.pem \ + tests/key_encrypted.pem \ tests/res_query_dump.c ## Examples diff --git a/examples/complex.c b/examples/complex.c index 00305ee..67c5d01 100644 --- a/examples/complex.c +++ b/examples/complex.c @@ -12,6 +12,7 @@ #include #include #include +#include #ifdef _WIN32 #include @@ -93,6 +94,52 @@ static int certfail_handler(const xmpp_tlscert_t *cert, return read_char[0] == 'y' || read_char[0] == 'Y'; } +static int +password_callback(char *pw, size_t pw_max, const char *fname, void *userdata) +{ + (void)userdata; + /* when using an encrypted keyfile, we get asked multiple times for the + * password ... cache it until we're connected + */ + static struct { + char pass[256]; + unsigned char hash[XMPP_SHA1_DIGEST_SIZE]; + size_t passlen, fnamelen; + unsigned count; + } password_cache; + int ret = -1; + unsigned char hash[XMPP_SHA1_DIGEST_SIZE]; + + size_t fname_len = strlen(fname); + xmpp_sha1_digest((void *)fname, fname_len, hash); + if (fname_len && fname_len == password_cache.fnamelen && + memcmp(hash, password_cache.hash, sizeof(hash)) == 0) { + if (password_cache.passlen && --password_cache.count) { + memcpy(pw, password_cache.pass, password_cache.passlen + 1); + ret = password_cache.passlen; + if (!password_cache.count) + memset(&password_cache, 0, sizeof(password_cache)); + return ret; + } + } + printf("Trying to unlock %s\n", fname); + char *pass = getpass("Please enter password: "); + if (!pass) + return -1; + size_t passlen = strlen(pass); + if (passlen < pw_max) { + memcpy(pw, pass, passlen + 1); + ret = passlen; + memcpy(password_cache.pass, pass, passlen + 1); + password_cache.passlen = passlen; + password_cache.count = 6; + memcpy(password_cache.hash, hash, sizeof(hash)); + password_cache.fnamelen = fname_len; + } + memset(pass, 0, passlen); + return ret; +} + static int sockopt_cb(xmpp_conn_t *conn, void *socket) { int timeout = KA_TIMEOUT; @@ -181,7 +228,7 @@ static void usage(int exit_code) " --jid The JID to use to authenticate.\n" " --pass The password of the JID.\n" " --tls-cert Path to client certificate.\n" - " --tls-key Path to private key.\n\n" + " --tls-key Path to private key or P12 file.\n\n" " --capath Path to an additional CA trust store " "(directory).\n" " --cafile Path to an additional CA trust store " @@ -247,7 +294,7 @@ int main(int argc, char **argv) else break; } - if ((!jid && (!cert || !key)) || (argc - i) > 2) { + if ((!jid && !key) || (argc - i) > 2) { usage(1); } @@ -279,8 +326,10 @@ int main(int argc, char **argv) if (tcp_keepalive) xmpp_conn_set_sockopt_callback(conn, sockopt_cb); + /* ask for a password if key is protected */ + xmpp_conn_set_password_callback(conn, password_callback, conn); /* setup authentication information */ - if (cert && key) { + if (key) { xmpp_conn_set_client_cert(conn, cert, key); } if (jid) diff --git a/src/auth.c b/src/auth.c index eaaf3cc..24a6291 100644 --- a/src/auth.c +++ b/src/auth.c @@ -242,7 +242,7 @@ _handle_features(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata) if (strcasecmp(text, "PLAIN") == 0) conn->sasl_support |= SASL_MASK_PLAIN; else if (strcasecmp(text, "EXTERNAL") == 0 && - conn->tls_client_cert) + (conn->tls_client_cert || conn->tls_client_key)) conn->sasl_support |= SASL_MASK_EXTERNAL; else if (strcasecmp(text, "DIGEST-MD5") == 0) conn->sasl_support |= SASL_MASK_DIGESTMD5; diff --git a/src/common.h b/src/common.h index 1b60b1b..964fb5f 100644 --- a/src/common.h +++ b/src/common.h @@ -192,6 +192,8 @@ struct _xmpp_conn_t { int auth_legacy_enabled; int secured; /* set when stream is secured with TLS */ xmpp_certfail_handler certfail_handler; + xmpp_password_callback password_callback; + void *password_callback_userdata; /* if server returns or we must do them */ int bind_required; diff --git a/src/conn.c b/src/conn.c index b9e1ffe..261c3cb 100644 --- a/src/conn.c +++ b/src/conn.c @@ -182,6 +182,8 @@ xmpp_conn_t *xmpp_conn_new(xmpp_ctx_t *ctx) conn->auth_legacy_enabled = 0; conn->secured = 0; conn->certfail_handler = NULL; + conn->password_callback = NULL; + conn->password_callback_userdata = NULL; conn->bind_required = 0; conn->session_required = 0; @@ -492,15 +494,38 @@ xmpp_tlscert_t *xmpp_conn_get_peer_cert(xmpp_conn_t *const conn) return tls_peer_cert(conn); } -/** Set the Client Certificate and Private Key that will be bound to the - * connection. If any of the both was previously set, it will be discarded. - * This should not be used after a connection is created. The function will - * make a copy of the strings passed in. - * Currently only non-encrypted Private Keys are supported. +/** Set the Callback function which will be called when the TLS stack can't + * decrypt a password protected key file. + * + * @param conn a Strophe connection object + * @param cb The callback function that shall be called + * @param userdata An opaque data pointer that will be passed to the callback + * + * @ingroup TLS + */ +void xmpp_conn_set_password_callback(xmpp_conn_t *conn, + xmpp_password_callback cb, + void *userdata) +{ + conn->password_callback = cb; + conn->password_callback_userdata = userdata; +} + +/** Set the Client Certificate and Private Key or PKCS#12 encoded file that + * will be bound to the connection. If any of them was previously set, it + * will be discarded. This should not be used after a connection is created. + * The function will make a copy of the strings passed in. + * + * In case the Private Key is encrypted, a callback must be set via + * \ref xmpp_conn_set_password_callback so the TLS stack can retrieve the + * password. + * + * In case one wants to use a PKCS#12 encoded file, it must be passed via + * the `key` parameter and `cert` must be NULL. * * @param conn a Strophe connection object * @param cert path to a certificate file - * @param key path to a private key file + * @param key path to a private key file or a P12 file * * @ingroup TLS */ @@ -511,7 +536,10 @@ void xmpp_conn_set_client_cert(xmpp_conn_t *const conn, strophe_debug(conn->ctx, "conn", "set client cert %s %s", cert, key); if (conn->tls_client_cert) strophe_free(conn->ctx, conn->tls_client_cert); - conn->tls_client_cert = strophe_strdup(conn->ctx, cert); + if (cert) + conn->tls_client_cert = strophe_strdup(conn->ctx, cert); + else + conn->tls_client_cert = NULL; if (conn->tls_client_key) strophe_free(conn->ctx, conn->tls_client_key); conn->tls_client_key = strophe_strdup(conn->ctx, key); @@ -620,7 +648,7 @@ int xmpp_connect_client(xmpp_conn_t *conn, int found = XMPP_DOMAIN_NOT_FOUND; int rc; - if (!conn->jid && conn->tls_client_cert) { + if (!conn->jid && (conn->tls_client_cert || conn->tls_client_key)) { if (tls_id_on_xmppaddr_num(conn) != 1) { strophe_debug(conn->ctx, "xmpp", "Client certificate contains multiple or no xmppAddr " diff --git a/src/tls_gnutls.c b/src/tls_gnutls.c index b56810e..7c506f5 100644 --- a/src/tls_gnutls.c +++ b/src/tls_gnutls.c @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include "common.h" #include "tls.h" @@ -47,10 +49,30 @@ void tls_shutdown(void) gnutls_global_deinit(); } -static gnutls_x509_crt_t _tls_load_cert(xmpp_conn_t *conn) +static int _tls_password_callback(void *userdata, + int attempt, + const char *token_url, + const char *token_label, + unsigned int flags, + char *pin, + size_t pin_max) +{ + xmpp_conn_t *conn = userdata; + UNUSED(attempt); + UNUSED(token_url); + UNUSED(token_label); + UNUSED(flags); + if (!conn->password_callback) { + strophe_error(conn->ctx, "tls", "No password callback set"); + return -1; + } + int ret = conn->password_callback(pin, pin_max, conn->tls_client_key, + conn->password_callback_userdata); + return ret > 0 ? 0 : GNUTLS_E_PKCS11_PIN_ERROR; +} + +static gnutls_x509_crt_t _tls_load_cert_x509(xmpp_conn_t *conn) { - if (conn->tls && conn->tls->client_cert) - return conn->tls->client_cert; gnutls_x509_crt_t cert; gnutls_datum_t data; int res; @@ -68,6 +90,64 @@ error_out: return NULL; } +static gnutls_x509_crt_t _tls_load_cert_p12(xmpp_conn_t *conn) +{ + gnutls_pkcs12_t p12; + gnutls_x509_crt_t *cert; + gnutls_datum_t data; + gnutls_x509_privkey_t key; + unsigned int cert_num = 0; + if (gnutls_pkcs12_init(&p12) < 0) + return NULL; + if (gnutls_load_file(conn->tls_client_key, &data) < 0) + goto error_out; + if (gnutls_pkcs12_import(p12, &data, GNUTLS_X509_FMT_DER, 0) < 0) + goto error_out2; + + char pass[GNUTLS_PKCS11_MAX_PIN_LEN]; + pass[0] = '\0'; + int passlen = + _tls_password_callback(conn, 0, NULL, NULL, 0, pass, sizeof(pass)); + if (passlen < 0) + pass[0] = '\0'; + + int res = gnutls_pkcs12_simple_parse(p12, pass, &key, &cert, &cert_num, + NULL, NULL, NULL, 0); + gnutls_pkcs12_deinit(p12); + gnutls_free(data.data); + if (res < 0) + goto error_out; + gnutls_x509_privkey_deinit(key); + if (cert_num > 1) { + strophe_error(conn->ctx, "tls", "Can't handle stack of %u certs", + cert_num); + goto error_out; + } + gnutls_x509_crt_t ret = *cert; + gnutls_free(cert); + return ret; +error_out2: + gnutls_free(data.data); +error_out: + if (cert) { + for (unsigned int n = 0; n < cert_num; ++n) { + gnutls_x509_crt_deinit(cert[n]); + } + gnutls_free(cert); + } + return NULL; +} + +static gnutls_x509_crt_t _tls_load_cert(xmpp_conn_t *conn) +{ + if (conn->tls && conn->tls->client_cert) + return conn->tls->client_cert; + if (!conn->tls_client_cert && conn->tls_client_key) { + return _tls_load_cert_p12(conn); + } + return _tls_load_cert_x509(conn); +} + static void _tls_free_cert(xmpp_conn_t *conn, gnutls_x509_crt_t cert) { if (conn->tls && conn->tls->client_cert == cert) @@ -269,12 +349,15 @@ static int _tls_verify(gnutls_session_t session) /* Return early if the Certificate is trusted * OR if we trust all Certificates */ - if (status == 0 || tls->conn->tls_trust) + if (status == 0 || tls->conn->tls_trust) { + gnutls_free(out.data); return 0; + } if (!tls->conn->certfail_handler) { strophe_error(tls->ctx, "tls", "No certfail handler set, canceling connection attempt"); + gnutls_free(out.data); return -1; } @@ -324,6 +407,9 @@ tls_t *tls_new(xmpp_conn_t *conn) gnutls_certificate_allocate_credentials(&tls->cred); tls_set_credentials(tls, NULL); + gnutls_certificate_set_pin_function(tls->cred, _tls_password_callback, + conn); + if (conn->tls_client_cert && conn->tls_client_key) { tls->client_cert = _tls_load_cert(conn); if (!tls->client_cert) { @@ -337,6 +423,15 @@ tls_t *tls_new(xmpp_conn_t *conn) gnutls_certificate_set_x509_key_file( tls->cred, conn->tls_client_cert, conn->tls_client_key, GNUTLS_X509_FMT_PEM); + } else if (conn->tls_client_key) { + char pass[GNUTLS_PKCS11_MAX_PIN_LEN]; + pass[0] = '\0'; + int passlen = _tls_password_callback(conn, 0, NULL, NULL, 0, pass, + sizeof(pass)); + if (passlen < 0) + pass[0] = '\0'; + gnutls_certificate_set_x509_simple_pkcs12_file( + tls->cred, conn->tls_client_key, GNUTLS_X509_FMT_DER, pass); } gnutls_certificate_set_verify_function(tls->cred, _tls_verify); diff --git a/src/tls_openssl.c b/src/tls_openssl.c index ab962b6..4dde48b 100644 --- a/src/tls_openssl.c +++ b/src/tls_openssl.c @@ -26,6 +26,7 @@ #include #include #include +#include #include "common.h" #include "tls.h" @@ -54,6 +55,30 @@ static const unsigned char *ASN1_STRING_get0_data(ASN1_STRING *asn1) } #endif +#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined LIBRESSL_VERSION_NUMBER +static int SSL_CTX_use_cert_and_key(SSL_CTX *ctx, + X509 *x509, + EVP_PKEY *privatekey, + STACK_OF(X509) * chain, + int override) +{ + UNUSED(override); + if (!ctx) + return 0; + if (x509 && !SSL_CTX_use_certificate(ctx, x509)) + return 0; + if (privatekey && !SSL_CTX_use_PrivateKey(ctx, privatekey)) + return 0; +#ifdef SSL_CTX_set1_chain + if (chain && !SSL_CTX_set1_chain(ctx, chain)) + return 0; +#else + UNUSED(chain); +#endif + return 1; +} +#endif + #if OPENSSL_VERSION_NUMBER < 0x10000000L static int GENERAL_NAME_get0_otherName(const GENERAL_NAME *gen, ASN1_OBJECT **poid, @@ -90,6 +115,8 @@ static void _tls_set_error(tls_t *tls, int error); static void _tls_log_error(xmpp_ctx_t *ctx); static void _tls_dump_cert_info(tls_t *tls); static X509 *_tls_cert_read(xmpp_conn_t *conn); +static X509 * +_tls_cert_read_p12(xmpp_conn_t *conn, EVP_PKEY **pkey, STACK_OF(X509) * *ca); static int _tls_xaddr_nid(void); static int _tls_xmppaddr_to_string(GENERAL_NAME *name, char **res); static int _tls_dnsname_to_string(GENERAL_NAME *name, char **res); @@ -260,8 +287,10 @@ char *tls_id_on_xmppaddr(xmpp_conn_t *conn, unsigned int n) char *ret = NULL; int i, j; GENERAL_NAMES *names = _tls_conn_get_names(conn); - if (!names) + if (!names) { + _tls_log_error(conn->ctx); return NULL; + } int num_names = sk_GENERAL_NAME_num(names); for (i = j = 0; i < num_names; ++i) { char *res; @@ -288,8 +317,10 @@ unsigned int tls_id_on_xmppaddr_num(xmpp_conn_t *conn) { unsigned int ret = 0; GENERAL_NAMES *names = _tls_conn_get_names(conn); - if (!names) + if (!names) { + _tls_log_error(conn->ctx); return 0; + } int j, num_names = sk_GENERAL_NAME_num(names); for (j = 0; j < num_names; ++j) { GENERAL_NAME *name = sk_GENERAL_NAME_value(names, j); @@ -355,18 +386,16 @@ _get_fingerprint(const xmpp_ctx_t *ctx, X509 *err_cert, xmpp_cert_element_t el) static char * _get_alg(const xmpp_ctx_t *ctx, X509 *err_cert, xmpp_cert_element_t el) { - int rc, alg_nid = NID_undef; + int alg_nid = NID_undef; switch (el) { case XMPP_CERT_KEYALG: { #if OPENSSL_VERSION_NUMBER < 0x10100000L - UNUSED(rc); alg_nid = OBJ_obj2nid(err_cert->cert_info->key->algor->algorithm); #else X509_PUBKEY *pubkey = X509_get_X509_PUBKEY(err_cert); ASN1_OBJECT *ppkalg = NULL; - rc = X509_PUBKEY_get0_param(&ppkalg, NULL, NULL, NULL, pubkey); - if (rc) { + if (X509_PUBKEY_get0_param(&ppkalg, NULL, NULL, NULL, pubkey)) { alg_nid = OBJ_obj2nid(ppkalg); } #endif @@ -510,6 +539,18 @@ static int _tls_verify(int preverify_ok, X509_STORE_CTX *x509_ctx) return ret; } +static int _tls_password_callback(char *buf, int size, int rwflag, void *u) +{ + xmpp_conn_t *conn = u; + UNUSED(rwflag); + if (!conn->password_callback) { + strophe_error(conn->ctx, "tls", "No password callback set"); + return -1; + } + return conn->password_callback(buf, (size_t)size, conn->tls_client_key, + conn->password_callback_userdata); +} + tls_t *tls_new(xmpp_conn_t *conn) { tls_t *tls = strophe_alloc(conn->ctx, sizeof(*tls)); @@ -539,6 +580,8 @@ tls_t *tls_new(xmpp_conn_t *conn) SSL_CTX_set_options(tls->ssl_ctx, SSL_OP_NO_SSLv2); /* DROWN */ SSL_CTX_set_options(tls->ssl_ctx, SSL_OP_NO_SSLv3); /* POODLE */ SSL_CTX_set_options(tls->ssl_ctx, SSL_OP_NO_TLSv1); /* BEAST */ + SSL_CTX_set_default_passwd_cb(tls->ssl_ctx, _tls_password_callback); + SSL_CTX_set_default_passwd_cb_userdata(tls->ssl_ctx, conn); if (conn->tls_client_cert && conn->tls_client_key) { tls->client_cert = _tls_cert_read(conn); @@ -552,6 +595,21 @@ tls_t *tls_new(xmpp_conn_t *conn) SSL_FILETYPE_PEM); SSL_CTX_use_PrivateKey_file(tls->ssl_ctx, conn->tls_client_key, SSL_FILETYPE_PEM); + } else if (conn->tls_client_key) { + EVP_PKEY *pkey = NULL; + STACK_OF(X509) *ca = NULL; + X509 *cert = _tls_cert_read_p12(conn, &pkey, &ca); + if (!cert) { + goto err_free_ctx; + } + + SSL_CTX_use_cert_and_key(tls->ssl_ctx, cert, pkey, ca, 1); + + if (pkey) + EVP_PKEY_free(pkey); + if (ca) + sk_X509_pop_free(ca, X509_free); + tls->client_cert = cert; } else { /* If the server asks for a client certificate, don't send one. */ SSL_CTX_set_client_cert_cb(tls->ssl_ctx, NULL); @@ -848,7 +906,7 @@ static void _tls_dump_cert_info(tls_t *tls) } } -static X509 *_tls_cert_read(xmpp_conn_t *conn) +static X509 *_tls_cert_read_x509(xmpp_conn_t *conn) { if (conn->tls && conn->tls->client_cert) return conn->tls->client_cert; @@ -865,6 +923,60 @@ static X509 *_tls_cert_read(xmpp_conn_t *conn) return c; } +static X509 * +_tls_cert_read_p12(xmpp_conn_t *conn, EVP_PKEY **pkey, STACK_OF(X509) * *ca) +{ + if (conn->tls && conn->tls->client_cert && !pkey && !ca) + return conn->tls->client_cert; + BIO *f = BIO_new_file(conn->tls_client_key, "r"); + if (!f) { + strophe_debug(conn->ctx, "tls", "f == NULL"); + goto error_out; + } + PKCS12 *p12 = d2i_PKCS12_bio(f, NULL); + BIO_free(f); + if (!p12) { + strophe_debug(conn->ctx, "tls", "Could not read p12 file"); + goto error_out; + } + + char pass[PEM_BUFSIZE]; + int passlen = _tls_password_callback(pass, sizeof(pass), 0, conn); + if (passlen <= 0) + pass[0] = '\0'; + + /* For some reason `PKCS12_parse()` fails without a `EVP_PKEY` + * so if the user doesn't want it, use a local one and free it + * again directly after parsing. + */ + EVP_PKEY *pkey_; + if (!pkey) + pkey = &pkey_; + X509 *cert = NULL; + int parse_ok = PKCS12_parse(p12, pass, pkey, &cert, ca); + if (pkey == &pkey_ && pkey_) + EVP_PKEY_free(pkey_); + PKCS12_free(p12); + if (!parse_ok) { + strophe_debug(conn->ctx, "tls", "Could not parse PKCS#12"); + goto error_out; + } + return cert; +error_out: + _tls_log_error(conn->ctx); + return NULL; +} + +static X509 *_tls_cert_read(xmpp_conn_t *conn) +{ + if (conn->tls && conn->tls->client_cert) + return conn->tls->client_cert; + if (!conn->tls_client_cert && conn->tls_client_key) { + return _tls_cert_read_p12(conn, NULL, NULL); + } + return _tls_cert_read_x509(conn); +} + static int _tls_xaddr_nid(void) { static int xaddr_nid = NID_undef; diff --git a/strophe.h b/strophe.h index 7a5fe1c..69bd3b4 100644 --- a/strophe.h +++ b/strophe.h @@ -269,6 +269,39 @@ typedef void (*xmpp_conn_handler)(xmpp_conn_t *conn, typedef int (*xmpp_certfail_handler)(const xmpp_tlscert_t *cert, const char *const errormsg); +/** The Handler function which will be called when the TLS stack can't + * decrypt a password protected key file. + * + * When this callback is called it shall write a NULL-terminated + * string of maximum length `pw_max - 1` to `pw`. + * + * This is currently only supported for GnuTLS and OpenSSL. + * + * On 2022-02-02 the following maximum lengths are valid: + * ``` + * include/gnutls/pkcs11.h: #define GNUTLS_PKCS11_MAX_PIN_LEN 256 + * include/openssl/pem.h: #define PEM_BUFSIZE 1024 + * ``` + * + * The usable length for GnuTLS is 255 as it expects a NULL-terminated string. + * + * The usable length for OpenSSL is 1024. + * + * + * @param pw The buffer where the password shall be stored. + * @param pw_max The maximum length of the password. + * @param fname The name of the file that is going to be unlocked. + * @param userdata The userdata pointer as supplied when setting this callback. + * + * @return -1 on error, else the number of bytes written to `pw` + * + * @ingroup TLS + */ +typedef int (*xmpp_password_callback)(char *pw, + size_t pw_max, + const char *fname, + void *userdata); + /** The function which will be called when Strophe creates a new socket. * * The `sock` argument is a pointer that is dependent on the architecture @@ -319,6 +352,9 @@ void xmpp_conn_set_capath(xmpp_conn_t *const conn, const char *path); void xmpp_conn_set_certfail_handler(xmpp_conn_t *const conn, xmpp_certfail_handler hndl); xmpp_tlscert_t *xmpp_conn_get_peer_cert(xmpp_conn_t *const conn); +void xmpp_conn_set_password_callback(xmpp_conn_t *conn, + xmpp_password_callback cb, + void *userdata); void xmpp_conn_set_client_cert(xmpp_conn_t *conn, const char *cert, const char *key); diff --git a/tests/cert.pfx b/tests/cert.pfx new file mode 100644 index 0000000000000000000000000000000000000000..6d55fd856d29851d1268a6210c93797dfa7e2b18 GIT binary patch literal 1246 zcmXqLV!6e{$ZXKWvV@IOtIebBJ1-+UqwM)G2?A5RC|z zy~+P0!I5Kw(9Q+I2PA&>%&5Ek%TeY>Yryh z`92_bZn4$b^#>+}vK{@|JUO39q}$5Va!bKmt&H2D-Y;a1Z;-TZySV#gc1JE#j`ptY zVGWnJ&Rw|f#QWNXJSs1>tK?e#G2eIk?{cgCiAd2-r$VMF5uTDMhW7O;e9c8xGrh$+ z4@_LT%XxvuV~@z00?P$f?rHt$e(L0oJulwxvtD{VYEp^WmxXbHySD89xH|vsoBbhN zX*+njO^@81z0FBN!ufFFJ!PgNS8s_|XUWZ9c5lzj#Zy-AnEfv4V&L6!H|3NQ4Jq%o zdVM>yH-P=;#_6;Ddw$d_?M|BzvHAD!{sq>33vNtvKmO!J1-JFRt?$ip-b}bS|KDc4 zg&MC51$n1_)migDZ?(@WfB$FQsv91enu#5Jo4?_{%8UPxH|%fltv8Zi-2Uy}Q{SGX z%`0vG{&4-jGW6o|HRqK}G)}&n*3rexnIZB_!?HXy;hMTl&HOuMjwZ&!p8R%qdv0uc zQ+RJuR__%%eK*!N&eXrr5%NDKymr$xZf8%*diAnKg2Pprf2Q~`X2H_+-nOo5s?K!= z@o{WUj9$c??mp>b>udKJdE(t%OmnB{ALE|>NcK=&)Y@J9R+ch_TOHZ>V{Nv1qFzyb z{v!oD?Gv{R{#+|}5B~Q5_}znf(+^iNEw2^hihYv(S1(3-mXARbqZ(31TF}HO&eFsv z3>4-EVs18WShnI~WLnTT+n{jBNK?%KMmv&0mG_6&GzjAS`5DWgE&Rx?%eb#pn|p&NEYH)u-krO3aGCTskfBr>Ms+ z1Lw>&5{7XG%5a}>iW-Wsh{SVkjjUO;xBI$b_Zz?JE%~vY@w*Mo5sE|%wd6DyN*GcZ zih*P@kS+q!3JkhHoXb$kkOSmp0@=X~jtqVVx(12{oNTPxe9TNztPCt77pxBceW$CI cnlWpk_?ro#y9x}o99cMQJ2IZPr