From e1291f040d13f205d4bb3c03bca28fef7312ff5e Mon Sep 17 00:00:00 2001 From: Sjoerd Simons Date: Sun, 28 Apr 2013 19:01:02 +0200 Subject: Port to TLS handling code to work in Idle Instead of server-tls-manager being a Wocky TLS manager add async API to start the certificate verification on request and use a GTlsCertificate to get the needed certificate information instead of WockyTLSsession. --- src/Makefile.am | 6 ++ src/idle-debug.c | 1 + src/idle-debug.h | 1 + src/server-tls-channel.c | 88 ++++++++++---------- src/server-tls-manager.c | 207 +++++++++-------------------------------------- src/server-tls-manager.h | 15 +++- src/tls-certificate.c | 8 +- 7 files changed, 105 insertions(+), 221 deletions(-) diff --git a/src/Makefile.am b/src/Makefile.am index 7326f8e..7ed3126 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -38,6 +38,12 @@ libidle_convenience_la_SOURCES = \ idle-server-connection.h \ idle-text.h \ idle-text.c \ + server-tls-channel.c \ + server-tls-channel.h \ + server-tls-manager.c \ + server-tls-manager.h \ + tls-certificate.c \ + tls-certificate.h \ $(NULL) nodist_libidle_convenience_la_SOURCES = \ diff --git a/src/idle-debug.c b/src/idle-debug.c index e8a2854..9b7ffcb 100644 --- a/src/idle-debug.c +++ b/src/idle-debug.c @@ -34,6 +34,7 @@ static GDebugKey _keys[] = { {"network", IDLE_DEBUG_NETWORK}, {"parser", IDLE_DEBUG_PARSER}, {"text", IDLE_DEBUG_TEXT}, + {"tls", IDLE_DEBUG_TLS}, {NULL, 0} }; diff --git a/src/idle-debug.h b/src/idle-debug.h index 9564bc0..ac933bf 100644 --- a/src/idle-debug.h +++ b/src/idle-debug.h @@ -31,6 +31,7 @@ typedef enum { IDLE_DEBUG_PARSER = (1 << 5), IDLE_DEBUG_TEXT = (1 << 6), IDLE_DEBUG_ROOMLIST = (1 << 7), + IDLE_DEBUG_TLS = (1 << 8), } IdleDebugFlags; void idle_debug_init (void); diff --git a/src/server-tls-channel.c b/src/server-tls-channel.c index 552b315..85fb845 100644 --- a/src/server-tls-channel.c +++ b/src/server-tls-channel.c @@ -22,14 +22,12 @@ #include "server-tls-channel.h" +#include #include #include -#include - -#define DEBUG_FLAG IDLE_DEBUG_TLS -#include "debug.h" -#include "connection.h" +#define IDLE_DEBUG_FLAG IDLE_DEBUG_TLS +#include "idle-debug.h" #include "tls-certificate.h" G_DEFINE_TYPE_WITH_CODE (IdleServerTLSChannel, idle_server_tls_channel, @@ -46,13 +44,13 @@ enum { PROP_REFERENCE_IDENTITIES, /* not exported */ - PROP_TLS_SESSION, + PROP_CERTIFICATE, NUM_PROPERTIES }; struct _IdleServerTLSChannelPrivate { - WockyTLSSession *tls_session; + GTlsCertificate *certificate; IdleTLSCertificate *server_cert; gchar *server_cert_path; @@ -81,8 +79,8 @@ idle_server_tls_channel_get_property (GObject *object, case PROP_REFERENCE_IDENTITIES: g_value_set_boxed (value, self->priv->reference_identities); break; - case PROP_TLS_SESSION: - g_value_set_object (value, self->priv->tls_session); + case PROP_CERTIFICATE: + g_value_set_object (value, self->priv->certificate); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); @@ -100,8 +98,8 @@ idle_server_tls_channel_set_property (GObject *object, switch (property_id) { - case PROP_TLS_SESSION: - self->priv->tls_session = g_value_dup_object (value); + case PROP_CERTIFICATE: + self->priv->certificate = g_value_dup_object (value); break; case PROP_HOSTNAME: self->priv->hostname = g_value_dup_string (value); @@ -120,7 +118,7 @@ idle_server_tls_channel_finalize (GObject *object) { IdleServerTLSChannel *self = IDLE_SERVER_TLS_CHANNEL (object); - DEBUG ("Finalize TLS channel"); + IDLE_DEBUG ("Finalize TLS channel"); g_free (self->priv->server_cert_path); g_free (self->priv->hostname); @@ -137,48 +135,27 @@ idle_server_tls_channel_dispose (GObject *object) if (self->priv->dispose_has_run) return; - DEBUG ("Dispose TLS channel"); + IDLE_DEBUG ("Dispose TLS channel"); self->priv->dispose_has_run = TRUE; tp_clear_object (&self->priv->server_cert); - tp_clear_object (&self->priv->tls_session); + tp_clear_object (&self->priv->certificate); G_OBJECT_CLASS (idle_server_tls_channel_parent_class)->dispose (object); } -static const gchar * -cert_type_to_str (WockyTLSCertType type) -{ - const gchar *retval = NULL; - - switch (type) - { - case WOCKY_TLS_CERT_TYPE_X509: - retval = "x509"; - break; - case WOCKY_TLS_CERT_TYPE_OPENPGP: - retval = "pgp"; - break; - default: - break; - } - - return retval; -} - static void idle_server_tls_channel_constructed (GObject *object) { IdleServerTLSChannel *self = IDLE_SERVER_TLS_CHANNEL (object); TpBaseChannel *base = TP_BASE_CHANNEL (self); - TpBaseConnection *base_conn = tp_base_channel_get_connection (base); void (*chain_up) (GObject *) = G_OBJECT_CLASS (idle_server_tls_channel_parent_class)->constructed; - WockyTLSCertType cert_type; const gchar *path; gchar *cert_object_path; GPtrArray *certificates; + GTlsCertificate *cert; if (chain_up != NULL) chain_up (object); @@ -188,18 +165,37 @@ idle_server_tls_channel_constructed (GObject *object) /* create the TLS certificate object */ path = tp_base_channel_get_object_path (base); cert_object_path = g_strdup_printf ("%s/TLSCertificateObject", path); - certificates = wocky_tls_session_get_peers_certificate ( - self->priv->tls_session, &cert_type); + certificates = g_ptr_array_new (); + + /* Setup the full chain */ + cert = self->priv->certificate; + while (cert != NULL) + { + GByteArray *content; + GArray *c; + + g_object_get (cert, "certificate", &content, NULL); + c = g_array_sized_new (TRUE, TRUE, sizeof (guchar), content->len); + g_array_append_vals (c, content->data, content->len); + g_ptr_array_add (certificates, c); + + g_byte_array_unref (content); + + cert = g_tls_certificate_get_issuer (cert); + } self->priv->server_cert = g_object_new (IDLE_TYPE_TLS_CERTIFICATE, "object-path", cert_object_path, "certificate-chain-data", certificates, - "certificate-type", cert_type_to_str (cert_type), - "dbus-daemon", IDLE_CONNECTION (base_conn)->daemon, + "certificate-type", "x509", + "dbus-daemon", + tp_base_connection_get_dbus_daemon ( + tp_base_channel_get_connection (TP_BASE_CHANNEL (self))), NULL); self->priv->server_cert_path = cert_object_path; + g_ptr_array_unref (certificates); - DEBUG ("Server TLS channel constructed at %s", path); + IDLE_DEBUG ("Server TLS channel constructed at %s", path); } static void @@ -282,11 +278,11 @@ idle_server_tls_channel_class_init (IdleServerTLSChannelClass *klass) G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); g_object_class_install_property (oclass, PROP_REFERENCE_IDENTITIES, pspec); - pspec = g_param_spec_object ("tls-session", "The WockyTLSSession", - "The WockyTLSSession object containing the TLS information", - WOCKY_TYPE_TLS_SESSION, + pspec = g_param_spec_object ("certificate", "The GTLSCertificate", + "The GTLSCertificate object containing the TLS information", + G_TYPE_TLS_CERTIFICATE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); - g_object_class_install_property (oclass, PROP_TLS_SESSION, pspec); + g_object_class_install_property (oclass, PROP_CERTIFICATE, pspec); tp_dbus_properties_mixin_implement_interface (oclass, TP_IFACE_QUARK_CHANNEL_TYPE_SERVER_TLS_CONNECTION, @@ -297,7 +293,7 @@ idle_server_tls_channel_class_init (IdleServerTLSChannelClass *klass) static void idle_server_tls_channel_close (TpBaseChannel *base) { - DEBUG ("Close() called on the TLS channel %p", base); + IDLE_DEBUG ("Close() called on the TLS channel %p", base); tp_base_channel_destroyed (base); } diff --git a/src/server-tls-manager.c b/src/server-tls-manager.c index e32c991..5073c82 100644 --- a/src/server-tls-manager.c +++ b/src/server-tls-manager.c @@ -24,41 +24,30 @@ #include #include -#define DEBUG_FLAG IDLE_DEBUG_TLS -#include "debug.h" -#include "gabble/caps-channel-manager.h" -#include "connection.h" +#define IDLE_DEBUG_FLAG IDLE_DEBUG_TLS +#include "idle-debug.h" +#include "idle-connection.h" #include "server-tls-channel.h" -#include "util.h" #include "extensions/extensions.h" -#include - static void channel_manager_iface_init (gpointer, gpointer); G_DEFINE_TYPE_WITH_CODE (IdleServerTLSManager, idle_server_tls_manager, - WOCKY_TYPE_TLS_HANDLER, + G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (TP_TYPE_CHANNEL_MANAGER, - channel_manager_iface_init); - G_IMPLEMENT_INTERFACE (IDLE_TYPE_CAPS_CHANNEL_MANAGER, - NULL)); + channel_manager_iface_init)); enum { PROP_CONNECTION = 1, - PROP_INTERACTIVE_TLS, NUM_PROPERTIES }; struct _IdleServerTLSManagerPrivate { /* Properties */ IdleConnection *connection; - gboolean interactive_tls; /* Current operation data */ - gchar *peername; - GStrv reference_identities; - WockyTLSSession *tls_session; IdleServerTLSChannel *channel; GSimpleAsyncResult *async_result; @@ -84,9 +73,6 @@ idle_server_tls_manager_get_property (GObject *object, case PROP_CONNECTION: g_value_set_object (value, self->priv->connection); break; - case PROP_INTERACTIVE_TLS: - g_value_set_boolean (value, self->priv->interactive_tls); - break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; @@ -106,9 +92,6 @@ idle_server_tls_manager_set_property (GObject *object, case PROP_CONNECTION: self->priv->connection = g_value_dup_object (value); break; - case PROP_INTERACTIVE_TLS: - self->priv->interactive_tls = g_value_get_boolean (value); - break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; @@ -144,7 +127,7 @@ connection_status_changed_cb (IdleConnection *conn, { IdleServerTLSManager *self = user_data; - DEBUG ("Connection status changed, now %d", status); + IDLE_DEBUG ("Connection status changed, now %d", status); if (status == TP_CONNECTION_STATUS_DISCONNECTED) { @@ -167,45 +150,21 @@ complete_verify (IdleServerTLSManager *self) g_simple_async_result_complete (self->priv->async_result); /* Reset to initial state */ - tp_clear_pointer (&self->priv->peername, g_free); - tp_clear_pointer (&self->priv->reference_identities, g_strfreev); - g_clear_object (&self->priv->tls_session); g_clear_object (&self->priv->channel); g_clear_object (&self->priv->async_result); } -static void -verify_fallback_cb (GObject *source, - GAsyncResult *result, - gpointer user_data) -{ - IdleServerTLSManager *self = (IdleServerTLSManager *) source; - GError *error = NULL; - - if (!chainup->verify_finish_func (WOCKY_TLS_HANDLER (self), result, &error)) - g_simple_async_result_take_error (self->priv->async_result, error); - - complete_verify (self); -} - static void server_tls_channel_closed_cb (IdleServerTLSChannel *channel, gpointer user_data) { IdleServerTLSManager *self = user_data; - DEBUG ("Server TLS channel closed."); + IDLE_DEBUG ("Server TLS channel closed."); if (channel == self->priv->channel) { - /* fallback to the old-style non interactive TLS verification */ - DEBUG ("Channel closed, but unhandled, falling back..."); - - chainup->verify_async_func (WOCKY_TLS_HANDLER (self), - self->priv->tls_session, self->priv->peername, - self->priv->reference_identities, verify_fallback_cb, NULL); - - self->priv->channel = NULL; + complete_verify (self); } else { @@ -240,7 +199,7 @@ tls_certificate_accepted_cb (IdleTLSCertificate *certificate, { IdleServerTLSManager *self = user_data; - DEBUG ("TLS certificate accepted"); + IDLE_DEBUG ("TLS certificate accepted"); complete_verify (self); } @@ -252,7 +211,7 @@ tls_certificate_rejected_cb (IdleTLSCertificate *certificate, { IdleServerTLSManager *self = user_data; - DEBUG ("TLS certificate rejected with rejections %p, length %u.", + IDLE_DEBUG ("TLS certificate rejected with rejections %p, length %u.", rejections, rejections->len); g_simple_async_result_set_error (self->priv->async_result, @@ -261,91 +220,27 @@ tls_certificate_rejected_cb (IdleTLSCertificate *certificate, complete_verify (self); } -static void -extend_string_ptr_array (GPtrArray *array, - GStrv new_elements) -{ - gint i; - - if (new_elements != NULL) - { - for (i = 0; new_elements[i] != NULL; i++) - { - if (!tp_str_empty (new_elements[i])) - g_ptr_array_add (array, g_strdup (new_elements[i])); - } - } -} - -static void -fill_reference_identities (IdleServerTLSManager *self, - const gchar *peername, - GStrv original_extra_identities) -{ - GPtrArray *identities; - gchar *connect_server = NULL; - gchar *explicit_server = NULL; - GStrv extra_certificate_identities = NULL; - - g_return_if_fail (self->priv->reference_identities == NULL); - - g_object_get (self->priv->connection, - "connect-server", &connect_server, - "explicit-server", &explicit_server, - "extra-certificate-identities", &extra_certificate_identities, - NULL); - - identities = g_ptr_array_new (); - - /* The peer name, i.e, the domain part of the JID */ - g_ptr_array_add (identities, g_strdup (peername)); - - /* The extra identities that the caller of verify_async() passed */ - extend_string_ptr_array (identities, original_extra_identities); - - /* The explicitly overridden server (if in use) */ - if (!tp_str_empty (explicit_server) && - !tp_strdiff (connect_server, explicit_server)) - { - g_ptr_array_add (identities, g_strdup (explicit_server)); - } - - /* Extra identities added to the account as a result of user choices */ - extend_string_ptr_array (identities, extra_certificate_identities); - - /* Null terminate, since this is a gchar** */ - g_ptr_array_add (identities, NULL); - - self->priv->reference_identities = (GStrv) g_ptr_array_free (identities, - FALSE); - - g_strfreev (extra_certificate_identities); - g_free (explicit_server); - g_free (connect_server); -} - -static void -idle_server_tls_manager_verify_async (WockyTLSHandler *handler, - WockyTLSSession *tls_session, +void +idle_server_tls_manager_verify_async (IdleServerTLSManager *self, + GTlsCertificate *certificate, const gchar *peername, - GStrv extra_identities, GAsyncReadyCallback callback, gpointer user_data) { - IdleServerTLSManager *self = IDLE_SERVER_TLS_MANAGER (handler); - IdleTLSCertificate *certificate; + IdleTLSCertificate *cert; GSimpleAsyncResult *result; + const gchar *identities[] = { peername, NULL }; g_return_if_fail (self->priv->async_result == NULL); - DEBUG ("verify_async() called on the IdleServerTLSManager."); + IDLE_DEBUG ("verify_async() called on the IdleServerTLSManager."); result = g_simple_async_result_new (G_OBJECT (self), callback, user_data, idle_server_tls_manager_verify_async); if (self->priv->connection == NULL) { - DEBUG ("connection already went away; failing immediately"); + IDLE_DEBUG ("connection already went away; failing immediately"); g_simple_async_result_set_error (result, TP_ERROR, TP_ERROR_CANCELLED, "The Telepathy connection has already been disconnected"); g_simple_async_result_complete_in_idle (result); @@ -355,37 +250,21 @@ idle_server_tls_manager_verify_async (WockyTLSHandler *handler, self->priv->async_result = result; - fill_reference_identities (self, peername, extra_identities); - - if (!self->priv->interactive_tls) - { - DEBUG ("ignore-ssl-errors is set, fallback to non-interactive " - "verification."); - - chainup->verify_async_func (WOCKY_TLS_HANDLER (self), tls_session, - peername, self->priv->reference_identities, verify_fallback_cb, NULL); - - return; - } - - self->priv->tls_session = g_object_ref (tls_session); - self->priv->peername = g_strdup (peername); - self->priv->channel = g_object_new (IDLE_TYPE_SERVER_TLS_CHANNEL, "connection", self->priv->connection, - "tls-session", tls_session, + "certificate", certificate, "hostname", peername, - "reference-identities", self->priv->reference_identities, + "reference-identities", identities, NULL); g_signal_connect (self->priv->channel, "closed", G_CALLBACK (server_tls_channel_closed_cb), self); - certificate = idle_server_tls_channel_get_certificate (self->priv->channel); + cert = idle_server_tls_channel_get_certificate (self->priv->channel); - g_signal_connect (certificate, "accepted", + g_signal_connect (cert, "accepted", G_CALLBACK (tls_certificate_accepted_cb), self); - g_signal_connect (certificate, "rejected", + g_signal_connect (cert, "rejected", G_CALLBACK (tls_certificate_rejected_cb), self); /* emit NewChannel on the ChannelManager iface */ @@ -393,12 +272,18 @@ idle_server_tls_manager_verify_async (WockyTLSHandler *handler, (TpExportableChannel *) self->priv->channel, NULL); } -static gboolean -idle_server_tls_manager_verify_finish (WockyTLSHandler *self, +gboolean +idle_server_tls_manager_verify_finish (IdleServerTLSManager *self, GAsyncResult *result, GError **error) { - wocky_implement_finish_void (self, idle_server_tls_manager_verify_async); + if (g_simple_async_result_propagate_error ( + G_SIMPLE_ASYNC_RESULT (result), error)) + return FALSE; + + g_return_val_if_fail (g_simple_async_result_is_valid (result, + G_OBJECT(self), idle_server_tls_manager_verify_async), FALSE); + return TRUE; } static void @@ -413,14 +298,13 @@ idle_server_tls_manager_dispose (GObject *object) { IdleServerTLSManager *self = IDLE_SERVER_TLS_MANAGER (object); - DEBUG ("%p", self); + IDLE_DEBUG ("%p", self); if (self->priv->dispose_has_run) return; self->priv->dispose_has_run = TRUE; - tp_clear_object (&self->priv->tls_session); tp_clear_object (&self->priv->connection); G_OBJECT_CLASS (idle_server_tls_manager_parent_class)->dispose (object); @@ -431,13 +315,10 @@ idle_server_tls_manager_finalize (GObject *object) { IdleServerTLSManager *self = IDLE_SERVER_TLS_MANAGER (object); - DEBUG ("%p", self); + IDLE_DEBUG ("%p", self); close_all (self); - g_free (self->priv->peername); - g_strfreev (self->priv->reference_identities); - G_OBJECT_CLASS (idle_server_tls_manager_parent_class)->finalize (object); } @@ -451,17 +332,16 @@ idle_server_tls_manager_constructed (GObject *object) if (chain_up != NULL) chain_up (object); - DEBUG ("Server TLS Manager constructed"); + IDLE_DEBUG ("Server TLS Manager constructed"); - idle_signal_connect_weak (self->priv->connection, "status-changed", - G_CALLBACK (connection_status_changed_cb), object); + tp_g_signal_connect_object (self->priv->connection, "status-changed", + G_CALLBACK (connection_status_changed_cb), object, 0); } static void idle_server_tls_manager_class_init (IdleServerTLSManagerClass *klass) { GObjectClass *oclass = G_OBJECT_CLASS (klass); - WockyTLSHandlerClass *hclass = WOCKY_TLS_HANDLER_CLASS (klass); GParamSpec *pspec; g_type_class_add_private (klass, sizeof (IdleServerTLSManagerPrivate)); @@ -472,20 +352,11 @@ idle_server_tls_manager_class_init (IdleServerTLSManagerClass *klass) oclass->set_property = idle_server_tls_manager_set_property; oclass->get_property = idle_server_tls_manager_get_property; - hclass->verify_async_func = idle_server_tls_manager_verify_async; - hclass->verify_finish_func = idle_server_tls_manager_verify_finish; - - pspec = g_param_spec_object ("connection", "IdleConnection object", - "Idle connection object that owns this manager.", - IDLE_TYPE_CONNECTION, + pspec = g_param_spec_object ("connection", "Base connection object", + "base connection object that owns this manager.", + TP_TYPE_BASE_CONNECTION, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (oclass, PROP_CONNECTION, pspec); - - pspec = g_param_spec_boolean ("interactive-tls", "Interactive TLS setting", - "Whether interactive TLS certificate verification is enabled.", - FALSE, - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); - g_object_class_install_property (oclass, PROP_INTERACTIVE_TLS, pspec); } static void diff --git a/src/server-tls-manager.h b/src/server-tls-manager.h index a584e9f..9e33753 100644 --- a/src/server-tls-manager.h +++ b/src/server-tls-manager.h @@ -22,7 +22,6 @@ #define __IDLE_SERVER_TLS_MANAGER_H__ #include -#include #include #include "extensions/extensions.h" @@ -34,11 +33,11 @@ typedef struct _IdleServerTLSManagerClass IdleServerTLSManagerClass; typedef struct _IdleServerTLSManagerPrivate IdleServerTLSManagerPrivate; struct _IdleServerTLSManagerClass { - WockyTLSHandlerClass parent_class; + GObjectClass parent_class; }; struct _IdleServerTLSManager { - WockyTLSHandler parent; + GObject parent; IdleServerTLSManagerPrivate *priv; }; @@ -69,6 +68,16 @@ void idle_server_tls_manager_get_rejection_details ( GHashTable **details, TpConnectionStatusReason *reason); +void idle_server_tls_manager_verify_async (IdleServerTLSManager *self, + GTlsCertificate *certificate, + const gchar *peername, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean idle_server_tls_manager_verify_finish (IdleServerTLSManager *self, + GAsyncResult *result, + GError **error); + G_END_DECLS #endif /* #ifndef __IDLE_SERVER_TLS_MANAGER_H__ */ diff --git a/src/tls-certificate.c b/src/tls-certificate.c index a0abfa6..467d13e 100644 --- a/src/tls-certificate.c +++ b/src/tls-certificate.c @@ -24,8 +24,8 @@ #include #include -#define DEBUG_FLAG IDLE_DEBUG_TLS -#include "debug.h" +#define IDLE_DEBUG_FLAG IDLE_DEBUG_TLS +#include "idle-debug.h" static void tls_certificate_iface_init (gpointer g_iface, gpointer iface_data); @@ -260,7 +260,7 @@ idle_tls_certificate_accept (TpSvcAuthenticationTLSCertificate *cert, { IdleTLSCertificate *self = IDLE_TLS_CERTIFICATE (cert); - DEBUG ("Accept() called on the TLS certificate; current state %u", + IDLE_DEBUG ("Accept() called on the TLS certificate; current state %u", self->priv->cert_state); if (self->priv->cert_state != TP_TLS_CERTIFICATE_STATE_PENDING) @@ -289,7 +289,7 @@ idle_tls_certificate_reject (TpSvcAuthenticationTLSCertificate *cert, { IdleTLSCertificate *self = IDLE_TLS_CERTIFICATE (cert); - DEBUG ("Reject() called on the TLS certificate with rejections %p, " + IDLE_DEBUG ("Reject() called on the TLS certificate with rejections %p, " "length %u; current state %u", rejections, rejections->len, self->priv->cert_state); -- cgit v1.2.3