Commit d72d9eab authored by Carlos Garcia Campos's avatar Carlos Garcia Campos Committed by Carlos Garcia Campos
Browse files

message: add API to handle client side certificates

When SoupSession doesn't have a GTlsInteraction set, SoupMessage can
handle client side certificates. A new signal request-certificate is
emitted when the connection requests a certificate and
soup_message_set_tls_client_certificate() can be called to complete the
request.
parent e13c0911
......@@ -31,6 +31,7 @@ soup_message_get_connection_id
soup_message_get_remote_address
soup_message_get_tls_peer_certificate
soup_message_get_tls_peer_certificate_errors
soup_message_set_tls_client_certificate
<SUBSECTION>
soup_message_set_first_party
soup_message_get_first_party
......
......@@ -43,6 +43,7 @@ ignore_headers = [
'soup-client-message-io-http1.h',
'soup-client-message-io-http2.h',
'soup-body-input-stream-http2.h',
'soup-tls-interaction.h',
]
mkdb_args = [
......
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
/*
* soup-tls-interaction.c: TLS interaction implementation
*
* Copyright (C) 2021 Igalia S.L.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include "soup-tls-interaction.h"
struct _SoupTlsInteraction {
GTlsInteraction parent;
};
typedef struct {
SoupConnection *conn;
} SoupTlsInteractionPrivate;
G_DEFINE_TYPE_WITH_PRIVATE (SoupTlsInteraction, soup_tls_interaction, G_TYPE_TLS_INTERACTION)
static void
soup_tls_interaction_request_certificate_async (GTlsInteraction *tls_interaction,
GTlsConnection *connection,
GTlsCertificateRequestFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
SoupTlsInteractionPrivate *priv = soup_tls_interaction_get_instance_private (SOUP_TLS_INTERACTION (tls_interaction));
GTask *task;
task = g_task_new (tls_interaction, cancellable, callback, user_data);
if (priv->conn)
soup_connection_request_tls_certificate (priv->conn, connection, task);
else
g_task_return_int (task, G_TLS_INTERACTION_FAILED);
g_object_unref (task);
}
static GTlsInteractionResult
soup_tls_interaction_request_certificate_finish (GTlsInteraction *tls_interaction,
GAsyncResult *result,
GError **error)
{
int task_result;
task_result = g_task_propagate_int (G_TASK (result), error);
return task_result != -1 ? task_result : G_TLS_INTERACTION_FAILED;
}
static void
soup_tls_interaction_finalize (GObject *object)
{
SoupTlsInteractionPrivate *priv = soup_tls_interaction_get_instance_private (SOUP_TLS_INTERACTION (object));
if (priv->conn) {
g_object_remove_weak_pointer (G_OBJECT (priv->conn), (gpointer*)&priv->conn);
priv->conn = NULL;
}
G_OBJECT_CLASS (soup_tls_interaction_parent_class)->finalize (object);
}
static void
soup_tls_interaction_init (SoupTlsInteraction *interaction)
{
}
static void
soup_tls_interaction_class_init (SoupTlsInteractionClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GTlsInteractionClass *interaction_class = G_TLS_INTERACTION_CLASS (klass);
object_class->finalize = soup_tls_interaction_finalize;
interaction_class->request_certificate_async = soup_tls_interaction_request_certificate_async;
interaction_class->request_certificate_finish = soup_tls_interaction_request_certificate_finish;
}
GTlsInteraction *
soup_tls_interaction_new (SoupConnection *conn)
{
GTlsInteraction *interaction;
SoupTlsInteractionPrivate *priv;
interaction = g_object_new (SOUP_TYPE_TLS_INTERACTION, NULL);
priv = soup_tls_interaction_get_instance_private (SOUP_TLS_INTERACTION (interaction));
priv->conn = conn;
g_object_add_weak_pointer (G_OBJECT (priv->conn), (gpointer*)&priv->conn);
return interaction;
}
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
/*
* Copyright (C) 2021 Igalia S.L.
*/
#pragma once
#include "soup-connection.h"
#include <gio/gio.h>
G_BEGIN_DECLS
#define SOUP_TYPE_TLS_INTERACTION (soup_tls_interaction_get_type ())
G_DECLARE_FINAL_TYPE (SoupTlsInteraction, soup_tls_interaction, SOUP, TLS_INTERACTION, GTlsInteraction)
GType soup_tls_interaction_get_type (void);
GTlsInteraction *soup_tls_interaction_new (SoupConnection *conn);
G_END_DECLS
......@@ -11,6 +11,7 @@ soup_sources = [
'auth/soup-auth-negotiate.c',
'auth/soup-auth-manager.c',
'auth/soup-connection-auth.c',
'auth/soup-tls-interaction.c',
'cache/soup-cache.c',
'cache/soup-cache-client-input-stream.c',
......
......@@ -17,6 +17,7 @@
#include "soup-client-message-io-http2.h"
#include "soup-socket-properties.h"
#include "soup-private-enum-types.h"
#include "soup-tls-interaction.h"
#include <gio/gnetworking.h>
struct _SoupConnection {
......@@ -43,6 +44,8 @@ typedef struct {
guint in_use;
SoupHTTPVersion http_version;
GTlsCertificate *tls_client_cert;
GCancellable *cancellable;
} SoupConnectionPrivate;
......@@ -51,6 +54,7 @@ G_DEFINE_TYPE_WITH_PRIVATE (SoupConnection, soup_connection, G_TYPE_OBJECT)
enum {
EVENT,
ACCEPT_CERTIFICATE,
REQUEST_CERTIFICATE,
DISCONNECTED,
LAST_SIGNAL
};
......@@ -114,6 +118,7 @@ soup_connection_finalize (GObject *object)
}
g_clear_object (&priv->iostream);
g_clear_object (&priv->tls_client_cert);
G_OBJECT_CLASS (soup_connection_parent_class)->finalize (object);
}
......@@ -229,6 +234,16 @@ soup_connection_class_init (SoupConnectionClass *connection_class)
G_TYPE_BOOLEAN, 2,
G_TYPE_TLS_CERTIFICATE,
G_TYPE_TLS_CERTIFICATE_FLAGS);
signals[REQUEST_CERTIFICATE] =
g_signal_new ("request-certificate",
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_LAST,
0,
g_signal_accumulator_true_handled, NULL,
NULL,
G_TYPE_BOOLEAN, 2,
G_TYPE_TLS_CLIENT_CONNECTION,
G_TYPE_TASK);
signals[DISCONNECTED] =
g_signal_new ("disconnected",
G_OBJECT_CLASS_TYPE (object_class),
......@@ -503,6 +518,7 @@ new_tls_connection (SoupConnection *conn,
{
SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);
GTlsClientConnection *tls_connection;
GTlsInteraction *tls_interaction;
GPtrArray *advertised_protocols = g_ptr_array_sized_new (4);
// https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml
......@@ -513,12 +529,13 @@ new_tls_connection (SoupConnection *conn,
g_ptr_array_add (advertised_protocols, "http/1.0");
g_ptr_array_add (advertised_protocols, NULL);
tls_interaction = priv->socket_props->tls_interaction ? g_object_ref (priv->socket_props->tls_interaction) : soup_tls_interaction_new (conn);
tls_connection = g_initable_new (g_tls_backend_get_client_connection_type (g_tls_backend_get_default ()),
priv->cancellable, error,
"base-io-stream", connection,
"server-identity", priv->remote_connectable,
"require-close-notify", FALSE,
"interaction", priv->socket_props->tls_interaction,
"interaction", tls_interaction,
"advertised-protocols", advertised_protocols->pdata,
NULL);
......@@ -1120,6 +1137,69 @@ soup_connection_get_tls_certificate_errors (SoupConnection *conn)
return g_tls_connection_get_peer_certificate_errors (G_TLS_CONNECTION (priv->connection));
}
void
soup_connection_set_tls_client_certificate (SoupConnection *conn,
GTlsCertificate *certificate)
{
SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);
if (G_IS_TLS_CONNECTION (priv->connection)) {
g_tls_connection_set_certificate (G_TLS_CONNECTION (priv->connection),
certificate);
g_clear_object (&priv->tls_client_cert);
return;
}
if (priv->tls_client_cert == certificate)
return;
g_clear_object (&priv->tls_client_cert);
priv->tls_client_cert = g_object_ref (certificate);
}
void
soup_connection_request_tls_certificate (SoupConnection *conn,
GTlsConnection *connection,
GTask *task)
{
SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);
gboolean handled = FALSE;
if (!G_IS_TLS_CONNECTION (priv->connection) || G_TLS_CONNECTION (priv->connection) != connection) {
g_task_return_int (task, G_TLS_INTERACTION_FAILED);
return;
}
if (priv->tls_client_cert) {
soup_connection_complete_tls_certificate_request (conn,
priv->tls_client_cert,
g_object_ref (task));
g_clear_object (&priv->tls_client_cert);
return;
}
g_signal_emit (conn, signals[REQUEST_CERTIFICATE], 0, connection, task, &handled);
if (!handled)
g_task_return_int (task, G_TLS_INTERACTION_FAILED);
}
void
soup_connection_complete_tls_certificate_request (SoupConnection *conn,
GTlsCertificate *certificate,
GTask *task)
{
SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);
if (G_IS_TLS_CONNECTION (priv->connection) && certificate) {
g_tls_connection_set_certificate (G_TLS_CONNECTION (priv->connection),
certificate);
g_task_return_int (task, G_TLS_INTERACTION_HANDLED);
} else {
g_task_return_int (task, G_TLS_INTERACTION_FAILED);
}
g_object_unref (task);
}
guint64
soup_connection_get_id (SoupConnection *conn)
{
......
......@@ -65,6 +65,14 @@ SoupClientMessageIO *soup_connection_setup_message_io (SoupConnection *conn,
GTlsCertificate *soup_connection_get_tls_certificate (SoupConnection *conn);
GTlsCertificateFlags soup_connection_get_tls_certificate_errors (SoupConnection *conn);
void soup_connection_request_tls_certificate (SoupConnection *conn,
GTlsConnection *connection,
GTask *task);
void soup_connection_complete_tls_certificate_request (SoupConnection *conn,
GTlsCertificate *certificate,
GTask *task);
void soup_connection_set_tls_client_certificate (SoupConnection *conn,
GTlsCertificate *certificate);
guint64 soup_connection_get_id (SoupConnection *conn);
GSocketAddress *soup_connection_get_remote_address (SoupConnection *conn);
......
......@@ -89,6 +89,9 @@ typedef struct {
GTlsCertificate *tls_peer_certificate;
GTlsCertificateFlags tls_peer_certificate_errors;
GTlsCertificate *tls_client_certificate;
GTask *pending_tls_cert_request;
SoupMessagePriority priority;
gboolean is_top_level_navigation;
......@@ -119,6 +122,7 @@ enum {
AUTHENTICATE,
NETWORK_EVENT,
ACCEPT_CERTIFICATE,
REQUEST_CERTIFICATE,
HSTS_ENFORCED,
LAST_SIGNAL
......@@ -169,6 +173,11 @@ soup_message_finalize (GObject *object)
SoupMessage *msg = SOUP_MESSAGE (object);
SoupMessagePrivate *priv = soup_message_get_instance_private (msg);
if (priv->pending_tls_cert_request) {
g_task_return_int (priv->pending_tls_cert_request, G_TLS_INTERACTION_FAILED);
g_object_unref (priv->pending_tls_cert_request);
}
soup_message_set_connection (msg, NULL);
g_clear_pointer (&priv->uri, g_uri_unref);
......@@ -183,6 +192,7 @@ soup_message_finalize (GObject *object)
g_clear_object (&priv->tls_peer_certificate);
g_clear_object (&priv->remote_address);
g_clear_object (&priv->tls_client_certificate);
soup_message_headers_unref (priv->request_headers);
soup_message_headers_unref (priv->response_headers);
......@@ -594,6 +604,35 @@ soup_message_class_init (SoupMessageClass *message_class)
G_TYPE_TLS_CERTIFICATE,
G_TYPE_TLS_CERTIFICATE_FLAGS);
/**
* SoupMessage::request-certificate:
* @msg: the message
* @tls_connection: the #GTlsClientConnection
*
* Emitted during the @msg's connection TLS handshake when
* @tls_connection requests a certificate from the client.
* You can set the client certificate by calling
* soup_message_set_tls_client_certificate() and returning %TRUE.
* It's possible to handle the request asynchornously by returning
* %TRUE and call soup_message_set_tls_client_certificate() later
* once the certificate is available.
* Note that this signal is not emitted if #SoupSession::tls-interaction
* was set, or if soup_message_set_tls_client_certificate() was called
* before the connection TLS handshake started.
*
* Returns: %TRUE to handle the request, or %FALSE to make the connection
* fail with %G_TLS_ERROR_CERTIFICATE_REQUIRED.
*/
signals[REQUEST_CERTIFICATE] =
g_signal_new ("request-certificate",
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_LAST,
0,
g_signal_accumulator_true_handled, NULL,
NULL,
G_TYPE_BOOLEAN, 1,
G_TYPE_TLS_CLIENT_CONNECTION);
/**
* SoupMessage::hsts-enforced:
* @msg: the message
......@@ -1391,6 +1430,23 @@ re_emit_accept_certificate (SoupMessage *msg,
return accept;
}
static gboolean
re_emit_request_certificate (SoupMessage *msg,
GTlsClientConnection *tls_conn,
GTask *task)
{
SoupMessagePrivate *priv = soup_message_get_instance_private (msg);
gboolean handled = FALSE;
priv->pending_tls_cert_request = g_object_ref (task);
g_signal_emit (msg, signals[REQUEST_CERTIFICATE], 0, tls_conn, &handled);
if (!handled)
g_clear_object (&priv->pending_tls_cert_request);
return handled;
}
static void
re_emit_tls_certificate_changed (SoupMessage *msg,
GParamSpec *pspec,
......@@ -1421,6 +1477,13 @@ soup_message_set_connection (SoupMessage *msg,
if (priv->connection) {
g_signal_handlers_disconnect_by_data (priv->connection, msg);
priv->io_data = NULL;
if (priv->pending_tls_cert_request) {
soup_connection_complete_tls_certificate_request (priv->connection,
priv->tls_client_certificate,
g_steal_pointer (&priv->pending_tls_cert_request));
g_clear_object (&priv->tls_client_certificate);
}
g_object_remove_weak_pointer (G_OBJECT (priv->connection), (gpointer*)&priv->connection);
soup_connection_set_in_use (priv->connection, FALSE);
}
......@@ -1438,12 +1501,21 @@ soup_message_set_connection (SoupMessage *msg,
soup_connection_get_tls_certificate_errors (priv->connection));
soup_message_set_remote_address (msg, soup_connection_get_remote_address (priv->connection));
if (priv->tls_client_certificate) {
soup_connection_set_tls_client_certificate (priv->connection,
priv->tls_client_certificate);
g_clear_object (&priv->tls_client_certificate);
}
g_signal_connect_object (priv->connection, "event",
G_CALLBACK (re_emit_connection_event),
msg, G_CONNECT_SWAPPED);
g_signal_connect_object (priv->connection, "accept-certificate",
G_CALLBACK (re_emit_accept_certificate),
msg, G_CONNECT_SWAPPED);
g_signal_connect_object (priv->connection, "request-certificate",
G_CALLBACK (re_emit_request_certificate),
msg, G_CONNECT_SWAPPED);
g_signal_connect_object (priv->connection, "notify::tls-certificate",
G_CALLBACK (re_emit_tls_certificate_changed),
msg, G_CONNECT_SWAPPED);
......@@ -2100,6 +2172,49 @@ soup_message_get_tls_peer_certificate_errors (SoupMessage *msg)
return priv->tls_peer_certificate_errors;
}
/**
* soup_message_set_tls_client_certificate:
* @msg: a #SoupMessage
* @certificate: the #GTlsCertificate to set
*
* Sets the @certificate to be used by @msg's connection when a
* client certificate is requested during the TLS handshake.
* You can call this as a response to #SoupMessage::request-certificate
* signal, or before the connection is started.
* Note that the #GTlsCertificate set by this function will be ignored if
* #SoupSession::tls-interaction is not %NULL.
*/
void
soup_message_set_tls_client_certificate (SoupMessage *msg,
GTlsCertificate *certificate)
{
SoupMessagePrivate *priv;
g_return_if_fail (SOUP_IS_MESSAGE (msg));
g_return_if_fail (G_IS_TLS_CERTIFICATE (certificate));
priv = soup_message_get_instance_private (msg);
if (priv->pending_tls_cert_request) {
g_assert (SOUP_IS_CONNECTION (priv->connection));
soup_connection_complete_tls_certificate_request (priv->connection,
certificate,
g_steal_pointer (&priv->pending_tls_cert_request));
return;
}
if (priv->connection) {
soup_connection_set_tls_client_certificate (priv->connection,
certificate);
return;
}
if (priv->tls_client_certificate == certificate)
return;
g_clear_object (&priv->tls_client_certificate);
priv->tls_client_certificate = g_object_ref (certificate);
}
/**
* SoupMessagePriority:
* @SOUP_MESSAGE_PRIORITY_VERY_LOW: The lowest priority, the messages
......
......@@ -106,6 +106,10 @@ GTlsCertificate *soup_message_get_tls_peer_certificate (SoupMessage *
SOUP_AVAILABLE_IN_ALL
GTlsCertificateFlags soup_message_get_tls_peer_certificate_errors (SoupMessage *msg);
SOUP_AVAILABLE_IN_ALL
void soup_message_set_tls_client_certificate (SoupMessage *msg,
GTlsCertificate *certificate);
/* Specialized signal handlers */
SOUP_AVAILABLE_IN_ALL
......
......@@ -204,6 +204,138 @@ do_tls_interaction_test (gconstpointer data)
g_object_unref (certificate);
}
static gboolean
request_certificate_cb (SoupMessage *msg,
GTlsClientConnection *conn,
GTlsCertificate *certificate)
{
soup_message_set_tls_client_certificate (msg, certificate);
return TRUE;
}
typedef struct {
SoupMessage *msg;
GTlsCertificate *certificate;
} SetCertificateAsyncData;
static gboolean
set_certificate_idle_cb (SetCertificateAsyncData *data)
{
soup_message_set_tls_client_certificate (data->msg, data->certificate);
return FALSE;
}
static gboolean
request_certificate_async_cb (SoupMessage *msg,
GTlsClientConnection *conn,
GTlsCertificate *certificate)
{
SetCertificateAsyncData *data;
data = g_new (SetCertificateAsyncData, 1);
data->msg = msg;
data->certificate = certificate;
g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
(GSourceFunc)set_certificate_idle_cb,
data, g_free);
return TRUE;
}
static void
do_tls_interaction_msg_test (gconstpointer data)
{
SoupServer *server = (SoupServer *)data;
SoupSession *session;
SoupMessage *msg;
GBytes *body;
GTlsDatabase *tls_db;
GTlsCertificate *certificate;
GError *error = NULL;
SOUP_TEST_SKIP_IF_NO_TLS;
session = soup_test_session_new (NULL);
tls_db = soup_session_get_tls_database (session);
g_signal_connect (server, "request-started",
G_CALLBACK (server_request_started),
tls_db);
/* Not handling request-certificate signal */
msg = soup_message_new_from_uri ("GET", uri);
body = soup_test_session_async_send (session, msg, NULL, &error);
if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CONNECTION_CLOSED))
g_assert_error (error, G_TLS_ERROR, G_TLS_ERROR_CERTIFICATE_REQUIRED);
g_clear_error (&error);
g_bytes_unref (body);
g_object_unref (msg);
/* Handling the request-certificate signal synchronously */
g_object_get (server, "tls-certificate", &certificate, NULL);
g_assert_nonnull (certificate);
msg = soup_message_new_from_uri ("GET", uri);
g_signal_connect (msg, "request-certificate",
G_CALLBACK (request_certificate_cb),
certificate);
body = soup_test_session_async_send (session, msg, NULL, &error);
g_assert_no_error (error);
soup_test_assert_message_status (msg, SOUP_STATUS_OK);
g_clear_error (&error);
g_bytes_unref (body);
g_object_unref (msg);
/* Next load doesn't emit request-certificate because the connection is reused */
msg = soup_message_new_from_uri ("GET", uri);
body = soup_test_session_async_send (session, msg, NULL, &error);
g_assert_no_error (error);
soup_test_assert_message_status (msg, SOUP_STATUS_OK);
g_clear_error (&error);
g_bytes_unref (body);
g_object_unref (msg);
/* It fails for a new connection */
msg = soup_message_new_from_uri ("GET", uri);
soup_message_add_flags (msg, SOUP_MESSAGE_NEW_CONNECTION);
body = soup_test_session_async_send (session, msg, NULL, &error);
if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CONNECTION_CLOSED))
g_assert_error (error, G_TLS_ERROR, G_TLS_ERROR_CERTIFICATE_REQUIRED);
g_clear_error (&error);
g_bytes_unref (body);
g_object_unref (msg);
/* request-certificate is not emitted if the certificate is set before the load */
msg = soup_message_new_from_uri ("GET", uri);
soup_message_add_flags (msg, SOUP_MESSAGE_NEW_CONNECTION);
soup_message_set_tls_client_certificate (msg, certificate);
body = soup_test_session_async_send (session, msg, NULL, &error);
g_assert_no_error (error);
soup_test_assert_message_status (msg, SOUP_STATUS_OK);
g_clear_error (&error);
g_bytes_unref (body);
g_object_unref (msg);
/* Handling the request-certificate signal asynchronously */
msg = soup_message_new_from_uri ("GET", uri);
soup_message_add_flags (msg, SOUP_MESSAGE_NEW_CONNECTION);
g_signal_connect (msg, "request-certificate",
G_CALLBACK (request_certificate_async_cb),
certificate);
body = soup_test_session_async_send (session, msg, NULL, &error);
g_assert_no_error (error);
soup_test_assert_message_status (msg, SOUP_STATUS_OK);
g_clear_error (&error);
g_bytes_unref (body);
g_object_unref (msg);
g_signal_handlers_disconnect_by_data (server, tls_db);