/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
Copyright (C) 2011 Red Hat, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, see .
*/
#include "config.h"
#ifdef USE_SMARTCARD_012
#include
#endif
#include "spice-client.h"
#include "spice-common.h"
#include "spice-channel-priv.h"
#include "smartcard-manager.h"
#include "smartcard-manager-priv.h"
#include "spice-session-priv.h"
/**
* SECTION:channel-smartcard
* @short_description: smartcard authentication
* @title: Smartcard Channel
* @section_id:
* @see_also: #SpiceSmartcardManager, #SpiceSession
* @stability: Stable
* @include: spice-client.h
*
* The Spice protocol defines a set of messages to forward smartcard
* information from the Spice client to the VM. This channel handles
* these messages. While it's mainly focus on smartcard readers and
* smartcards, it's also possible to use it with a software smartcard
* (ie a set of 3 certificates from the client machine).
* This class doesn't provide useful methods, see #SpiceSession properties
* for a way to enable/disable this channel, and #SpiceSmartcardManager
* if you want to detect smartcard reader hotplug/unplug, and smartcard
* insertion/removal.
*/
#define SPICE_SMARTCARD_CHANNEL_GET_PRIVATE(obj) \
(G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_SMARTCARD_CHANNEL, SpiceSmartcardChannelPrivate))
struct _SpiceSmartcardChannelMessage {
#ifdef USE_SMARTCARD
VSCMsgType message_type;
#endif
SpiceMsgOut *message;
};
typedef struct _SpiceSmartcardChannelMessage SpiceSmartcardChannelMessage;
struct _SpiceSmartcardChannelPrivate {
/* track readers that have been added but for which we didn't receive
* an ack from the spice server yet. We rely on the fact that the
* readers in this list are ordered by the time we sent the request to
* the server. When we get an ack from the server for a reader addition,
* we can pop the 1st entry to get the reader the ack corresponds to. */
GList *pending_reader_additions;
/* used to removals of readers that were not ack'ed yet by the spice
* server */
GHashTable *pending_reader_removals;
/* used to track card insertions on readers that were not ack'ed yet
* by the spice server */
GHashTable *pending_card_insertions;
/* next commands to be sent to the spice server. This is needed since
* we have to wait for a command answer before sending the next one
*/
GQueue *message_queue;
/* message that is currently being processed by the spice server (ie last
* message that was sent to the server)
*/
SpiceSmartcardChannelMessage *in_flight_message;
};
G_DEFINE_TYPE(SpiceSmartcardChannel, spice_smartcard_channel, SPICE_TYPE_CHANNEL)
enum {
SPICE_SMARTCARD_LAST_SIGNAL,
};
static void spice_smartcard_channel_up(SpiceChannel *channel);
static void handle_smartcard_msg(SpiceChannel *channel, SpiceMsgIn *in);
static void smartcard_message_free(SpiceSmartcardChannelMessage *message);
/* ------------------------------------------------------------------ */
#ifdef USE_SMARTCARD
static void reader_added_cb(SpiceSmartcardManager *manager, VReader *reader,
gpointer user_data);
static void reader_removed_cb(SpiceSmartcardManager *manager, VReader *reader,
gpointer user_data);
static void card_inserted_cb(SpiceSmartcardManager *manager, VReader *reader,
gpointer user_data);
static void card_removed_cb(SpiceSmartcardManager *manager, VReader *reader,
gpointer user_data);
#endif
static void spice_smartcard_channel_init(SpiceSmartcardChannel *channel)
{
SpiceSmartcardChannelPrivate *priv;
channel->priv = SPICE_SMARTCARD_CHANNEL_GET_PRIVATE(channel);
priv = channel->priv;
priv->message_queue = g_queue_new();
#ifdef USE_SMARTCARD
priv->pending_card_insertions =
g_hash_table_new_full(g_direct_hash, g_direct_equal,
(GDestroyNotify)vreader_free, NULL);
priv->pending_reader_removals =
g_hash_table_new_full(g_direct_hash, g_direct_equal,
(GDestroyNotify)vreader_free, NULL);
#endif
}
static void spice_smartcard_channel_constructed(GObject *object)
{
SpiceSession *s = spice_channel_get_session(SPICE_CHANNEL(object));
g_return_if_fail(s != NULL);
#ifdef USE_SMARTCARD
if (!spice_session_is_for_migration(s)) {
SpiceSmartcardChannel *channel = SPICE_SMARTCARD_CHANNEL(object);
SpiceSmartcardManager *manager = spice_smartcard_manager_get();
spice_g_signal_connect_object(G_OBJECT(manager), "reader-added",
(GCallback)reader_added_cb, channel, 0);
spice_g_signal_connect_object(G_OBJECT(manager), "reader-removed",
(GCallback)reader_removed_cb, channel, 0);
spice_g_signal_connect_object(G_OBJECT(manager), "card-inserted",
(GCallback)card_inserted_cb, channel, 0);
spice_g_signal_connect_object(G_OBJECT(manager), "card-removed",
(GCallback)card_removed_cb, channel, 0);
}
#endif
if (G_OBJECT_CLASS(spice_smartcard_channel_parent_class)->constructed)
G_OBJECT_CLASS(spice_smartcard_channel_parent_class)->constructed(object);
}
static void spice_smartcard_channel_finalize(GObject *obj)
{
SpiceSmartcardChannel *channel = SPICE_SMARTCARD_CHANNEL(obj);
SpiceSmartcardChannelPrivate *c = channel->priv;
g_clear_pointer(&c->pending_card_insertions, g_hash_table_destroy);
g_clear_pointer(&c->pending_reader_removals, g_hash_table_destroy);
if (c->message_queue != NULL) {
g_queue_foreach(c->message_queue, (GFunc)smartcard_message_free, NULL);
g_queue_free(c->message_queue);
c->message_queue = NULL;
}
g_clear_pointer(&c->in_flight_message, smartcard_message_free);
g_clear_pointer(&c->pending_reader_additions, g_list_free);
if (G_OBJECT_CLASS(spice_smartcard_channel_parent_class)->finalize)
G_OBJECT_CLASS(spice_smartcard_channel_parent_class)->finalize(obj);
}
static void spice_smartcard_channel_reset(SpiceChannel *channel, gboolean migrating)
{
SpiceSmartcardChannel *smartcard_channel = SPICE_SMARTCARD_CHANNEL(channel);
SpiceSmartcardChannelPrivate *c = smartcard_channel->priv;
g_hash_table_remove_all(c->pending_card_insertions);
g_hash_table_remove_all(c->pending_reader_removals);
if (c->message_queue != NULL) {
g_queue_foreach(c->message_queue, (GFunc)smartcard_message_free, NULL);
g_queue_clear(c->message_queue);
}
g_clear_pointer(&c->in_flight_message, smartcard_message_free);
g_clear_pointer(&c->pending_reader_additions, g_list_free);
SPICE_CHANNEL_CLASS(spice_smartcard_channel_parent_class)->channel_reset(channel, migrating);
}
static void channel_set_handlers(SpiceChannelClass *klass)
{
static const spice_msg_handler handlers[] = {
[ SPICE_MSG_SMARTCARD_DATA ] = handle_smartcard_msg,
};
spice_channel_set_handlers(klass, handlers, G_N_ELEMENTS(handlers));
}
static void spice_smartcard_channel_class_init(SpiceSmartcardChannelClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
gobject_class->finalize = spice_smartcard_channel_finalize;
gobject_class->constructed = spice_smartcard_channel_constructed;
channel_class->channel_up = spice_smartcard_channel_up;
channel_class->channel_reset = spice_smartcard_channel_reset;
g_type_class_add_private(klass, sizeof(SpiceSmartcardChannelPrivate));
channel_set_handlers(SPICE_CHANNEL_CLASS(klass));
}
/* ------------------------------------------------------------------ */
/* private api */
static void
smartcard_message_free(SpiceSmartcardChannelMessage *message)
{
if (message->message)
spice_msg_out_unref(message->message);
g_free(message);
}
#ifdef USE_SMARTCARD
static gboolean is_attached_to_server(VReader *reader)
{
return (vreader_get_id(reader) != (vreader_id_t)-1);
}
static gboolean
spice_channel_has_pending_card_insertion(SpiceSmartcardChannel *channel,
VReader *reader)
{
return (g_hash_table_lookup(channel->priv->pending_card_insertions, reader) != NULL);
}
static void
spice_channel_queue_card_insertion(SpiceSmartcardChannel *channel,
VReader *reader)
{
vreader_reference(reader);
g_hash_table_insert(channel->priv->pending_card_insertions,
reader, reader);
}
static void
spice_channel_drop_pending_card_insertion(SpiceSmartcardChannel *channel,
VReader *reader)
{
g_hash_table_remove(channel->priv->pending_card_insertions, reader);
}
static gboolean
spice_channel_has_pending_reader_removal(SpiceSmartcardChannel *channel,
VReader *reader)
{
return (g_hash_table_lookup(channel->priv->pending_reader_removals, reader) != NULL);
}
static void
spice_channel_queue_reader_removal(SpiceSmartcardChannel *channel,
VReader *reader)
{
vreader_reference(reader);
g_hash_table_insert(channel->priv->pending_reader_removals,
reader, reader);
}
static void
spice_channel_drop_pending_reader_removal(SpiceSmartcardChannel *channel,
VReader *reader)
{
g_hash_table_remove(channel->priv->pending_reader_removals, reader);
}
static SpiceSmartcardChannelMessage *
smartcard_message_new(VSCMsgType msg_type, SpiceMsgOut *msg_out)
{
SpiceSmartcardChannelMessage *message;
message = g_new0(SpiceSmartcardChannelMessage, 1);
message->message = msg_out;
message->message_type = msg_type;
return message;
}
/* Indicates that handling of the message that is currently in flight has
* been completed. If needed, sends the next queued command to the server. */
static void
smartcard_message_complete_in_flight(SpiceSmartcardChannel *channel)
{
g_return_if_fail(channel->priv->in_flight_message != NULL);
smartcard_message_free(channel->priv->in_flight_message);
channel->priv->in_flight_message = g_queue_pop_head(channel->priv->message_queue);
if (channel->priv->in_flight_message != NULL) {
spice_msg_out_send(channel->priv->in_flight_message->message);
channel->priv->in_flight_message->message = NULL;
}
}
static void smartcard_message_send(SpiceSmartcardChannel *channel,
VSCMsgType msg_type,
SpiceMsgOut *msg_out, gboolean queue)
{
SpiceSmartcardChannelMessage *message;
if (spice_channel_get_read_only(SPICE_CHANNEL(channel)))
return;
CHANNEL_DEBUG(channel, "send message %u, %s",
msg_type, queue ? "queued" : "now");
if (!queue) {
spice_msg_out_send(msg_out);
return;
}
message = smartcard_message_new(msg_type, msg_out);
if (channel->priv->in_flight_message == NULL) {
g_return_if_fail(g_queue_is_empty(channel->priv->message_queue));
channel->priv->in_flight_message = message;
spice_msg_out_send(channel->priv->in_flight_message->message);
channel->priv->in_flight_message->message = NULL;
} else {
g_queue_push_tail(channel->priv->message_queue, message);
}
}
static void
send_msg_generic_with_data(SpiceSmartcardChannel *channel, VReader *reader,
VSCMsgType msg_type,
const uint8_t *data, gsize data_len,
gboolean serialize_msg)
{
SpiceMsgOut *msg_out;
VSCMsgHeader header = {
.type = msg_type,
.length = data_len
};
if(vreader_get_id(reader) == -1)
header.reader_id = VSCARD_UNDEFINED_READER_ID;
else
header.reader_id = vreader_get_id(reader);
msg_out = spice_msg_out_new(SPICE_CHANNEL(channel),
SPICE_MSGC_SMARTCARD_DATA);
msg_out->marshallers->msgc_smartcard_header(msg_out->marshaller, &header);
if ((data != NULL) && (data_len != 0)) {
spice_marshaller_add(msg_out->marshaller, data, data_len);
}
smartcard_message_send(channel, msg_type, msg_out, serialize_msg);
}
static void send_msg_generic(SpiceSmartcardChannel *channel, VReader *reader,
VSCMsgType msg_type)
{
send_msg_generic_with_data(channel, reader, msg_type, NULL, 0, TRUE);
}
static void send_msg_atr(SpiceSmartcardChannel *channel, VReader *reader)
{
#define MAX_ATR_LEN 40 //this should be defined in libcacard
uint8_t atr[MAX_ATR_LEN];
int atr_len = MAX_ATR_LEN;
g_return_if_fail(vreader_get_id(reader) != VSCARD_UNDEFINED_READER_ID);
vreader_power_on(reader, atr, &atr_len);
send_msg_generic_with_data(channel, reader, VSC_ATR, atr, atr_len, TRUE);
}
static void reader_added_cb(SpiceSmartcardManager *manager, VReader *reader,
gpointer user_data)
{
SpiceSmartcardChannel *channel = SPICE_SMARTCARD_CHANNEL(user_data);
const char *reader_name = vreader_get_name(reader);
if (vreader_get_id(reader) != -1 ||
g_list_find(channel->priv->pending_reader_additions, reader))
return;
channel->priv->pending_reader_additions =
g_list_append(channel->priv->pending_reader_additions, reader);
send_msg_generic_with_data(channel, reader, VSC_ReaderAdd,
(uint8_t*)reader_name, strlen(reader_name), TRUE);
}
static void reader_removed_cb(SpiceSmartcardManager *manager, VReader *reader,
gpointer user_data)
{
SpiceSmartcardChannel *channel = SPICE_SMARTCARD_CHANNEL(user_data);
if (is_attached_to_server(reader)) {
send_msg_generic(channel, reader, VSC_ReaderRemove);
} else {
spice_channel_queue_reader_removal(channel, reader);
}
}
/* ------------------------------------------------------------------ */
/* callbacks */
static void card_inserted_cb(SpiceSmartcardManager *manager, VReader *reader,
gpointer user_data)
{
SpiceSmartcardChannel *channel = SPICE_SMARTCARD_CHANNEL(user_data);
if (is_attached_to_server(reader)) {
send_msg_atr(channel, reader);
} else {
spice_channel_queue_card_insertion(channel, reader);
}
}
static void card_removed_cb(SpiceSmartcardManager *manager, VReader *reader,
gpointer user_data)
{
SpiceSmartcardChannel *channel = SPICE_SMARTCARD_CHANNEL(user_data);
if (is_attached_to_server(reader)) {
send_msg_generic(channel, reader, VSC_CardRemove);
} else {
/* this does nothing when reader has no card insertion pending */
spice_channel_drop_pending_card_insertion(channel, reader);
}
}
#endif /* USE_SMARTCARD */
static void spice_smartcard_channel_up_cb(GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
SpiceChannel *channel = SPICE_CHANNEL(user_data);
#ifdef USE_SMARTCARD
SpiceSmartcardManager *manager = spice_smartcard_manager_get();
GList *l, *list = NULL;
#endif
GError *error = NULL;
g_return_if_fail(channel != NULL);
g_return_if_fail(SPICE_IS_SESSION(source_object));
spice_smartcard_manager_init_finish(SPICE_SESSION(source_object),
res, &error);
if (error) {
g_warning("%s", error->message);
goto end;
}
#ifdef USE_SMARTCARD
list = spice_smartcard_manager_get_readers(manager);
for (l = list; l != NULL; l = l->next) {
VReader *reader = l->data;
gboolean has_card = vreader_card_is_present(reader) == VREADER_OK;
reader_added_cb(manager, reader, channel);
if (has_card)
card_inserted_cb(manager, reader, channel);
g_boxed_free(SPICE_TYPE_SMARTCARD_READER, reader);
}
#endif
end:
#ifdef USE_SMARTCARD
g_list_free(list);
#endif
g_clear_error(&error);
}
static void spice_smartcard_channel_up(SpiceChannel *channel)
{
if (spice_session_is_for_migration(spice_channel_get_session(channel)))
return;
spice_smartcard_manager_init_async(spice_channel_get_session(channel),
g_cancellable_new(),
spice_smartcard_channel_up_cb,
channel);
}
static void handle_smartcard_msg(SpiceChannel *channel, SpiceMsgIn *in)
{
#ifdef USE_SMARTCARD
SpiceSmartcardChannel *smartcard_channel = SPICE_SMARTCARD_CHANNEL(channel);
SpiceSmartcardChannelPrivate *priv = smartcard_channel->priv;
SpiceMsgSmartcard *msg = spice_msg_in_parsed(in);
VReader *reader;
CHANNEL_DEBUG(channel, "handle msg %u", msg->type);
switch (msg->type) {
case VSC_Error:
g_return_if_fail(priv->in_flight_message != NULL);
CHANNEL_DEBUG(channel, "in flight %u", priv->in_flight_message->message_type);
switch (priv->in_flight_message->message_type) {
case VSC_ReaderAdd:
g_return_if_fail(priv->pending_reader_additions != NULL);
reader = priv->pending_reader_additions->data;
g_return_if_fail(reader != NULL);
g_return_if_fail(vreader_get_id(reader) == -1);
priv->pending_reader_additions =
g_list_delete_link(priv->pending_reader_additions,
priv->pending_reader_additions);
vreader_set_id(reader, msg->reader_id);
if (spice_channel_has_pending_card_insertion(smartcard_channel, reader)) {
send_msg_atr(smartcard_channel, reader);
spice_channel_drop_pending_card_insertion(smartcard_channel, reader);
}
if (spice_channel_has_pending_reader_removal(smartcard_channel, reader)) {
send_msg_generic(smartcard_channel, reader, VSC_CardRemove);
spice_channel_drop_pending_reader_removal(smartcard_channel, reader);
}
break;
case VSC_APDU:
case VSC_ATR:
case VSC_CardRemove:
case VSC_Error:
case VSC_ReaderRemove:
break;
default:
g_warning("Unexpected message: %u", priv->in_flight_message->message_type);
break;
}
smartcard_message_complete_in_flight(smartcard_channel);
break;
case VSC_APDU:
case VSC_Init: {
const unsigned int APDU_BUFFER_SIZE = 270;
VReaderStatus reader_status;
uint8_t data_out[APDU_BUFFER_SIZE + sizeof(uint32_t)];
int data_out_len = sizeof(data_out);
g_return_if_fail(msg->reader_id != VSCARD_UNDEFINED_READER_ID);
reader = vreader_get_reader_by_id(msg->reader_id);
g_return_if_fail(reader != NULL); //FIXME: add log message
reader_status = vreader_xfr_bytes(reader,
msg->data, msg->length,
data_out, &data_out_len);
if (reader_status == VREADER_OK) {
send_msg_generic_with_data(smartcard_channel,
reader, VSC_APDU,
data_out, data_out_len, FALSE);
} else {
uint32_t error_code;
error_code = GUINT32_TO_LE(reader_status);
send_msg_generic_with_data(smartcard_channel,
reader, VSC_Error,
(uint8_t*)&error_code,
sizeof (error_code), FALSE);
}
break;
}
default:
g_return_if_reached();
}
#endif
}