From 18c95fa7bd6e82433ac3146ef573f02c1b966999 Mon Sep 17 00:00:00 2001 From: Steffen Jaeckel Date: Wed, 3 Mar 2021 13:57:09 +0100 Subject: [PATCH] add support for client authentication via certificates The SASL EXTERNAL method is implemented to make this possible. Signed-off-by: Steffen Jaeckel --- src/auth.c | 46 ++++++++++++ src/common.h | 3 + src/conn.c | 72 +++++++++++++++++++ src/tls.h | 3 + src/tls_openssl.c | 175 +++++++++++++++++++++++++++++++++++++++++++++- strophe.h | 5 ++ 6 files changed, 301 insertions(+), 3 deletions(-) diff --git a/src/auth.c b/src/auth.c index fcc10f6..6a2ce91 100644 --- a/src/auth.c +++ b/src/auth.c @@ -242,6 +242,9 @@ _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->sasl_support |= SASL_MASK_EXTERNAL; else if (strcasecmp(text, "DIGEST-MD5") == 0) conn->sasl_support |= SASL_MASK_DIGESTMD5; else if (strcasecmp(text, "SCRAM-SHA-1") == 0) @@ -651,6 +654,49 @@ static void _auth(xmpp_conn_t *conn) /* SASL ANONYMOUS was tried, unset flag */ conn->sasl_support &= ~SASL_MASK_ANONYMOUS; + } else if (conn->sasl_support & SASL_MASK_EXTERNAL) { + /* more crap here */ + auth = _make_sasl_auth(conn, "EXTERNAL"); + if (!auth) { + disconnect_mem_error(conn); + return; + } + + authdata = xmpp_stanza_new(conn->ctx); + if (!authdata) { + xmpp_stanza_release(auth); + disconnect_mem_error(conn); + return; + } + str = tls_id_on_xmppaddr(conn, 0); + if (!str || (tls_id_on_xmppaddr_num(conn) == 1 && + strcmp(str, conn->jid) == 0)) { + xmpp_stanza_set_text(authdata, "="); + } else { + xmpp_free(conn->ctx, str); + str = xmpp_base64_encode(conn->ctx, (void *)conn->jid, + strlen(conn->jid)); + if (!str) { + xmpp_stanza_release(authdata); + xmpp_stanza_release(auth); + disconnect_mem_error(conn); + return; + } + xmpp_stanza_set_text(authdata, str); + } + xmpp_free(conn->ctx, str); + + xmpp_stanza_add_child(auth, authdata); + xmpp_stanza_release(authdata); + + handler_add(conn, _handle_sasl_result, XMPP_NS_SASL, NULL, NULL, + "EXTERNAL"); + + xmpp_send(conn, auth); + xmpp_stanza_release(auth); + + /* SASL EXTERNAL was tried, unset flag */ + conn->sasl_support &= ~SASL_MASK_EXTERNAL; } else if (anonjid) { xmpp_error(conn->ctx, "auth", "No node in JID, and SASL ANONYMOUS unsupported."); diff --git a/src/common.h b/src/common.h index de3a6e1..01a6c8f 100644 --- a/src/common.h +++ b/src/common.h @@ -131,6 +131,7 @@ struct _xmpp_send_queue_t { #define SASL_MASK_SCRAMSHA1 (1 << 3) #define SASL_MASK_SCRAMSHA256 (1 << 4) #define SASL_MASK_SCRAMSHA512 (1 << 5) +#define SASL_MASK_EXTERNAL (1 << 6) #define SASL_MASK_SCRAM \ (SASL_MASK_SCRAMSHA1 | SASL_MASK_SCRAMSHA256 | SASL_MASK_SCRAMSHA512) @@ -164,6 +165,8 @@ struct _xmpp_conn_t { int tls_mandatory; int tls_legacy_ssl; int tls_trust; + char *tls_client_cert; + char *tls_client_key; int tls_failed; /* set when tls fails, so we don't try again */ int sasl_support; /* if true, field is a bitfield of supported mechanisms */ diff --git a/src/conn.c b/src/conn.c index d3bc8d8..5ae484d 100644 --- a/src/conn.c +++ b/src/conn.c @@ -143,6 +143,8 @@ xmpp_conn_t *xmpp_conn_new(xmpp_ctx_t *ctx) conn->tls_legacy_ssl = 0; conn->tls_trust = 0; conn->tls_failed = 0; + conn->tls_client_cert = NULL; + conn->tls_client_key = NULL; conn->sasl_support = 0; conn->auth_legacy_enabled = 0; conn->secured = 0; @@ -338,6 +340,10 @@ int xmpp_conn_release(xmpp_conn_t *conn) xmpp_free(ctx, conn->pass); if (conn->lang) xmpp_free(ctx, conn->lang); + if (conn->tls_client_cert) + xmpp_free(ctx, conn->tls_client_cert); + if (conn->tls_client_key) + xmpp_free(ctx, conn->tls_client_key); xmpp_free(ctx, conn); released = 1; } @@ -394,6 +400,58 @@ void xmpp_conn_set_jid(xmpp_conn_t *conn, const char *jid) conn->jid = xmpp_strdup(conn->ctx, jid); } +/** 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. + * + * @param conn a Strophe connection object + * @param cert path to a certificate file + * @param key path to a private key file + * + * @ingroup Connections + */ +void xmpp_conn_set_client_cert(xmpp_conn_t *const conn, + const char *const cert, + const char *const key) +{ + xmpp_debug(conn->ctx, "conn", "set client cert %s %s", cert, key); + if (conn->tls_client_cert) + xmpp_free(conn->ctx, conn->tls_client_cert); + conn->tls_client_cert = xmpp_strdup(conn->ctx, cert); + if (conn->tls_client_key) + xmpp_free(conn->ctx, conn->tls_client_key); + conn->tls_client_key = xmpp_strdup(conn->ctx, key); +} + +/** Get the number of xmppAddr entries in the client certificate. + * + * @param conn a Strophe connection object + * + * @return the number of xmppAddr entries in the client certificate + * + * @ingroup Connections + */ +unsigned int xmpp_conn_cert_xmppaddr_num(xmpp_conn_t *const conn) +{ + return tls_id_on_xmppaddr_num(conn); +} + +/** Get a specific xmppAddr entry. + * + * @param conn a Strophe connection object + * @param n the index of the entry, starting at 0 + * + * @return a string containing the xmppAddr or NULL if n is out of range + * + * @ingroup Connections + */ +char *xmpp_conn_cert_xmppaddr(xmpp_conn_t *const conn, unsigned int n) +{ + return tls_id_on_xmppaddr(conn, n); +} + /** Get the password used for authentication of a connection. * * @param conn a Strophe connection object @@ -470,6 +528,20 @@ int xmpp_connect_client(xmpp_conn_t *conn, int found = XMPP_DOMAIN_NOT_FOUND; int rc; + if (!conn->jid && conn->tls_client_cert) { + if (tls_id_on_xmppaddr_num(conn) != 1) { + xmpp_debug(conn->ctx, "xmpp", + "Client certificate contains multiple or no xmppAddr " + "and no JID was given to be used."); + return XMPP_EINVOP; + } + conn->jid = tls_id_on_xmppaddr(conn, 0); + if (!conn->jid) + return XMPP_EMEM; + xmpp_debug(conn->ctx, "xmpp", "Use jid %s from id-on-xmppAddr.", + conn->jid); + } + domain = xmpp_jid_domain(conn->ctx, conn->jid); if (!domain) return XMPP_EMEM; diff --git a/src/tls.h b/src/tls.h index 692f5c9..f840a75 100644 --- a/src/tls.h +++ b/src/tls.h @@ -27,6 +27,9 @@ void tls_shutdown(void); tls_t *tls_new(xmpp_conn_t *conn); void tls_free(tls_t *tls); +char *tls_id_on_xmppaddr(xmpp_conn_t *conn, unsigned int n); +unsigned int tls_id_on_xmppaddr_num(xmpp_conn_t *conn); + int tls_set_credentials(tls_t *tls, const char *cafilename); int tls_start(tls_t *tls); diff --git a/src/tls_openssl.c b/src/tls_openssl.c index 8f10f46..8f5fb00 100644 --- a/src/tls_openssl.c +++ b/src/tls_openssl.c @@ -47,11 +47,19 @@ #endif #endif +#if OPENSSL_VERSION_NUMBER < 0x10100000L +static const unsigned char *ASN1_STRING_get0_data(ASN1_STRING *asn1) +{ + return ASN1_STRING_data(asn1); +} +#endif + struct _tls { xmpp_ctx_t *ctx; sock_t sock; SSL_CTX *ssl_ctx; SSL *ssl; + X509 *client_cert; int lasterror; }; @@ -66,6 +74,10 @@ static const char *_tls_error_str(int error, const char **tbl, size_t tbl_size); 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 int _tls_xaddr_nid(void); +static int _tls_name_to_xmppaddr(GENERAL_NAME *name, char **res); +static GENERAL_NAMES *_tls_cert_get_names(xmpp_conn_t *conn); #define TLS_ERROR_STR(error, table) \ _tls_error_str(error, table, ARRAY_SIZE(table)) @@ -189,6 +201,8 @@ void tls_initialize(void) #else OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS, NULL); #endif + /* init xmppAddr OID */ + _tls_xaddr_nid(); } void tls_shutdown(void) @@ -199,6 +213,7 @@ void tls_shutdown(void) * memory rather than cause random crashes of the main program. */ #if OPENSSL_VERSION_NUMBER < 0x10100000L + OBJ_cleanup(); ERR_free_strings(); EVP_cleanup(); CRYPTO_cleanup_all_ex_data(); @@ -218,6 +233,55 @@ int tls_error(tls_t *tls) return tls->lasterror; } +/** Search through the SubjectAlternativeNames and return the next + * id-on-xmppAddr element starting from `n`. + */ +char *tls_id_on_xmppaddr(xmpp_conn_t *conn, unsigned int n) +{ + char *ret = NULL; + int i, j; + GENERAL_NAMES *names = _tls_cert_get_names(conn); + if (!names) + return NULL; + int num_names = sk_GENERAL_NAME_num(names); + for (i = j = 0; i < num_names; ++i) { + char *res; + GENERAL_NAME *name = sk_GENERAL_NAME_value(names, i); + if (name == NULL) + break; + if (_tls_name_to_xmppaddr(name, &res)) + continue; + if (j == (int)n) { + xmpp_debug(conn->ctx, "tls", "extracted jid %s from id-on-xmppAddr", + res); + ret = xmpp_strdup(conn->ctx, res); + OPENSSL_free(res); + break; + } + j++; + OPENSSL_free(res); + } + GENERAL_NAMES_free(names); + return ret; +} + +unsigned int tls_id_on_xmppaddr_num(xmpp_conn_t *conn) +{ + unsigned int ret = 0; + GENERAL_NAMES *names = _tls_cert_get_names(conn); + if (!names) + 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); + if (_tls_name_to_xmppaddr(name, NULL)) + continue; + ret++; + } + GENERAL_NAMES_free(names); + return ret; +} + tls_t *tls_new(xmpp_conn_t *conn) { tls_t *tls = xmpp_alloc(conn->ctx, sizeof(*tls)); @@ -249,7 +313,23 @@ tls_t *tls_new(xmpp_conn_t *conn) 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_client_cert_cb(tls->ssl_ctx, NULL); + if (conn->tls_client_cert && conn->tls_client_key) { + tls->client_cert = _tls_cert_read(conn); + if (!tls->client_cert) { + xmpp_error(tls->ctx, "tls", + "could not read client certificate"); + goto err_free_ctx; + } + + SSL_CTX_use_certificate_file(tls->ssl_ctx, conn->tls_client_cert, + SSL_FILETYPE_PEM); + SSL_CTX_use_PrivateKey_file(tls->ssl_ctx, conn->tls_client_key, + SSL_FILETYPE_PEM); + } else { + /* If the server asks for a client certificate, don't send one. */ + SSL_CTX_set_client_cert_cb(tls->ssl_ctx, NULL); + } + SSL_CTX_set_mode(tls->ssl_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE); ret = SSL_CTX_set_default_verify_paths(tls->ssl_ctx); @@ -261,12 +341,12 @@ tls_t *tls_new(xmpp_conn_t *conn) */ xmpp_error(tls->ctx, "tls", "SSL_CTX_set_default_verify_paths() failed"); - goto err_free_ctx; + goto err_free_cert; } tls->ssl = SSL_new(tls->ssl_ctx); if (tls->ssl == NULL) - goto err_free_ctx; + goto err_free_cert; #if OPENSSL_VERSION_NUMBER >= 0x0908060L && !defined(OPENSSL_NO_TLSEXT) /* Enable SNI. */ @@ -301,6 +381,8 @@ tls_t *tls_new(xmpp_conn_t *conn) err_free_ssl: SSL_free(tls->ssl); +err_free_cert: + X509_free(tls->client_cert); err_free_ctx: SSL_CTX_free(tls->ssl_ctx); err: @@ -312,6 +394,7 @@ err: void tls_free(tls_t *tls) { SSL_free(tls->ssl); + X509_free(tls->client_cert); SSL_CTX_free(tls->ssl_ctx); xmpp_free(tls->ctx, tls); } @@ -509,3 +592,89 @@ static void _tls_dump_cert_info(tls_t *tls) X509_free(cert); } } + +static X509 *_tls_cert_read(xmpp_conn_t *conn) +{ + if (conn->tls && conn->tls->client_cert) + return conn->tls->client_cert; + BIO *f = BIO_new_file(conn->tls_client_cert, "r"); + if (!f) { + xmpp_debug(conn->ctx, "tls", "f == NULL"); + return NULL; + } + X509 *c = PEM_read_bio_X509(f, NULL, NULL, NULL); + BIO_free(f); + if (!c) { + unsigned long error; + while ((error = ERR_get_error()) != 0) { + xmpp_debug(conn->ctx, "tls", "c == NULL: %s", + ERR_error_string(error, NULL)); + } + } + return c; +} + +static int _tls_xaddr_nid(void) +{ + static int xaddr_nid = NID_undef; + if (xaddr_nid == NID_undef) { + xaddr_nid = OBJ_create("1.3.6.1.5.5.7.8.5", "id-on-xmppAddr", + "XmppAddr Identifier"); + } + return xaddr_nid; +} + +static GENERAL_NAMES *_tls_cert_get_names(xmpp_conn_t *conn) +{ + X509 *client_cert; + GENERAL_NAMES *names = NULL; + client_cert = _tls_cert_read(conn); + if (!client_cert) + return NULL; + int san = X509_get_ext_by_NID(client_cert, NID_subject_alt_name, 0); + X509_EXTENSION *san_ext = X509_get_ext(client_cert, san); + if (!san_ext) + goto OUT; + ASN1_OCTET_STRING *data = X509_EXTENSION_get_data(san_ext); + if (!data) + goto OUT; + const unsigned char *d = ASN1_STRING_get0_data(data); + if (!d) + goto OUT; + names = d2i_GENERAL_NAMES(NULL, &d, ASN1_STRING_length(data)); +OUT: + if (!conn->tls || !conn->tls->client_cert) + X509_free(client_cert); + return names; +} + +/** Convert GENERAL_NAME* to a string + * + * This checks whether the GENERAL_NAME* that is given has the + * correct id-on-xmppAddr set and then optionally converts this + * form ASN.1 to a string/char*. + * + * When `res` pointer is set to NULL this method doesn't allocate + * the result but only checks whether it is in the correct format. + * + * @param name Pointer to the GENERAL_NAME that shall be converted + * @param res Result-pointer (optional, can be NULL) + * + * @return classic Unix style - 0=success, 1=error + */ +static int _tls_name_to_xmppaddr(GENERAL_NAME *name, char **res) +{ + ASN1_OBJECT *oid; + ASN1_TYPE *val; + if (!name || name->type != GEN_OTHERNAME) + return 1; + if (GENERAL_NAME_get0_otherName(name, &oid, &val) == 0) + return 1; + if (OBJ_obj2nid(oid) != _tls_xaddr_nid() || !val) + return 1; + if (!res) + return 0; + if (ASN1_STRING_to_UTF8((unsigned char **)res, val->value.asn1_string) < 0) + return 1; + return 0; +} diff --git a/strophe.h b/strophe.h index d38a8bc..e16ee08 100644 --- a/strophe.h +++ b/strophe.h @@ -234,6 +234,11 @@ int xmpp_conn_set_flags(xmpp_conn_t *conn, long flags); const char *xmpp_conn_get_jid(const xmpp_conn_t *conn); const char *xmpp_conn_get_bound_jid(const xmpp_conn_t *conn); void xmpp_conn_set_jid(xmpp_conn_t *conn, const char *jid); +void xmpp_conn_set_client_cert(xmpp_conn_t *conn, + const char *cert, + const char *key); +unsigned int xmpp_conn_cert_xmppaddr_num(xmpp_conn_t *conn); +char *xmpp_conn_cert_xmppaddr(xmpp_conn_t *conn, unsigned int n); const char *xmpp_conn_get_pass(const xmpp_conn_t *conn); void xmpp_conn_set_pass(xmpp_conn_t *conn, const char *pass); xmpp_ctx_t *xmpp_conn_get_context(xmpp_conn_t *conn);