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 <jaeckel-floss@eyet-services.de>
This commit is contained in:
Steffen Jaeckel
2022-02-03 09:11:49 +01:00
parent 06afe3d1c2
commit bddb80a192
12 changed files with 410 additions and 57 deletions

View File

@@ -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;

View File

@@ -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 <bind/> or <session/> we must do them */
int bind_required;

View File

@@ -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 "

View File

@@ -17,6 +17,8 @@
#include <gnutls/gnutls.h>
#include <gnutls/x509.h>
#include <gnutls/x509-ext.h>
#include <gnutls/pkcs11.h>
#include <gnutls/pkcs12.h>
#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);

View File

@@ -26,6 +26,7 @@
#include <openssl/err.h>
#include <openssl/opensslv.h>
#include <openssl/x509v3.h>
#include <openssl/pkcs12.h>
#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;