Hi Jakub, On Tue, Jul 31, 2018 at 4:50 PM, Jakub Jelen <jjelen@xxxxxxxxxx> wrote: > * This involves creation of properties structures in the applet, > that are used to discover pki buffers in the applet and its > properties. > * This also removes the old way of accessing certificates using > GET CERTIFICATE APDU > > Signed-off-by: Jakub Jelen <jjelen@xxxxxxxxxx> > Reviewed-by: Robert Relyea <rrelyea@xxxxxxxxxx> > --- Starting from this patch, make check fails. It eventually get fixed by later patch "tests: GP Card Manager and responses on SELECT APDU" but then tests/hwtest keeps failing. > src/cac.c | 467 +++++++++++++++++++++++++++++++++++++++++++++++------- > src/cac.h | 30 +++- > 2 files changed, 430 insertions(+), 67 deletions(-) > > diff --git a/src/cac.c b/src/cac.c > index 8adb7f3..4fa84e3 100644 > --- a/src/cac.c > +++ b/src/cac.c > @@ -17,13 +17,11 @@ > #include "vcard.h" > #include "vcard_emul.h" > #include "card_7816.h" > +#include "simpletlv.h" > +#include "common.h" > > /* private data for PKI applets */ > typedef struct CACPKIAppletDataStruct { > - unsigned char *cert; > - int cert_len; > - unsigned char *cert_buffer; > - int cert_buffer_len; > unsigned char *sign_buffer; > int sign_buffer_len; > VCardKey *key; > @@ -33,12 +31,146 @@ typedef struct CACPKIAppletDataStruct { > * CAC applet private data > */ > struct VCardAppletPrivateStruct { > + /* common attributes */ > + unsigned char *tag_buffer; > + int tag_buffer_len; > + unsigned char *val_buffer; > + int val_buffer_len; > + struct simpletlv_member *properties; > + unsigned int properties_len; > + /* applet-specific */ > union { > CACPKIAppletData pki_data; > void *reserved; > } u; > }; > > +/* > + * Encode SimpleTLV structures to file expected to be returned by the card. > + * This means, data in SimpleTLV prefixed with 2B encoding the length of > + * the whole buffer. > + */ > +static int > +cac_create_file(struct simpletlv_member *tlv, size_t tlv_len, > + unsigned char **out, int type) > +{ > + int len, length; > + unsigned char *buffer = NULL, *start; > + > + len = simpletlv_get_length(tlv, tlv_len, type); > + if (len < 0) > + goto failure; > + > + buffer = g_malloc(2 /*2B length*/ + len); > + > + start = buffer + 2; > + if (type == SIMPLETLV_TL) > + length = simpletlv_encode_tl(tlv, tlv_len, &start, len, NULL); > + else if (type == SIMPLETLV_VALUE) > + length = simpletlv_encode_val(tlv, tlv_len, &start, len, NULL); > + else > + goto failure; > + > + if (length <= 0) > + goto failure; > + > + ushort2lebytes(buffer, length); > + > + *out = buffer; > + return len + 2; > + > +failure: > + *out = NULL; > + g_free(buffer); > + return 0; > +} > + > +static inline int > +cac_create_tl_file(struct simpletlv_member *tlv, size_t tlv_len, > + unsigned char **out) > +{ > + return cac_create_file(tlv, tlv_len, out, SIMPLETLV_TL); > +} > + > +static inline int > +cac_create_val_file(struct simpletlv_member *tlv, size_t tlv_len, > + unsigned char **out) > +{ > + return cac_create_file(tlv, tlv_len, out, SIMPLETLV_VALUE); > +} > + > +/* > + * This function returns properties of an applet encoded as SimpleTLV. > + * If the tags argument is provided, only the tags in the passed list > + * with respective values are returned. > + * Otherwise, all the tags are returned. > + */ > +static VCardResponse * > +get_properties(VCard *card, > + struct simpletlv_member *properties, unsigned int properties_len, > + unsigned char *tags, unsigned int tags_len, > + unsigned int a_Le) > +{ > + VCardResponse *r = NULL; > + struct simpletlv_member *cp = NULL; > + unsigned int cp_len = 0; > + unsigned char *properties_buffer = NULL; > + unsigned int properties_buffer_len = 0; > + > + if (tags_len > 0 && tags) { > + unsigned int i, j, k = 0; > + > + cp = g_malloc_n(tags_len, sizeof(struct simpletlv_member)); > + > + /* show only matching */ > + for (j = 0; j < tags_len; j++) { > + int match = 0; > + for (i = 0; i < properties_len; i++) { > + if (properties[i].tag == tags[j]) { > + memcpy(&cp[k], &properties[i], > + sizeof(struct simpletlv_member)); > + match++; > + k++; > + break; // XXX do not allow more tags of the same ID > + } > + } > + /* if this tag was not found, return */ > + if (!match) { > + r = vcard_make_response(VCARD7816_STATUS_ERROR_DATA_NOT_FOUND); > + goto cleanup; > + } > + } > + cp_len = tags_len; > + } else { > + cp = properties; > + cp_len = properties_len; > + } > + > + /* Encode the SimpleTLV structure */ > + properties_buffer_len = simpletlv_encode(cp, cp_len, > + &properties_buffer, 0, NULL); > + if (properties_buffer_len <= 0) { > + g_debug("%s: Failed to encode properties buffer", __func__); > + goto cleanup; > + } > + > + if (a_Le > properties_buffer_len) { > + r = vcard_response_new_status_bytes( > + VCARD7816_SW1_LE_ERROR, properties_buffer_len); > + goto cleanup; > + } > + r = vcard_response_new(card, properties_buffer, properties_buffer_len, > + a_Le, VCARD7816_STATUS_SUCCESS); > + > +cleanup: > + g_free(properties_buffer); > + if (tags_len > 0 && tags) > + g_free(cp); > + if (r == NULL) > + r = vcard_make_response(VCARD7816_STATUS_ERROR_GENERAL); > + return r; > +} > + > /* > * handle all the APDU's that are common to all CAC applets > */ > @@ -46,9 +178,61 @@ static VCardStatus > cac_common_process_apdu(VCard *card, VCardAPDU *apdu, VCardResponse **response) > { > int ef; > + VCardAppletPrivate *applet_private; > VCardStatus ret = VCARD_FAIL; > > + applet_private = vcard_get_current_applet_private(card, apdu->a_channel); > + > switch (apdu->a_ins) { > + case CAC_GET_PROPERTIES: > + /* 5.3.3.4: Get Properties APDU */ > + assert(applet_private); > + > + if (apdu->a_p2 != 0x00) { > + /* P2 needs to be 0x00 */ > + *response = vcard_make_response( > + VCARD7816_STATUS_ERROR_P1_P2_INCORRECT); > + ret = VCARD_DONE; > + break; > + } > + switch (apdu->a_p1) { > + case 0x00: > + /* Get a GSC-IS v2.0 compatible properties response message. */ > + /* If P1 = 0x00 cannot be supported by the smart card, SW1 = 0x6A and SW2 = 86. */ > + *response = vcard_make_response( > + VCARD7816_STATUS_ERROR_P1_P2_INCORRECT); > + break; > + case 0x01: > + /* Get all the properties. */ > + if (apdu->a_Lc != 0) { > + *response = vcard_make_response( > + VCARD7816_STATUS_ERROR_DATA_INVALID); > + ret = VCARD_DONE; > + break; > + } > + *response = get_properties(card, applet_private->properties, > + applet_private->properties_len, NULL, 0, apdu->a_Le); > + break; > + case 0x02: > + /* Get the properties of the tags provided in list of tags in > + * the command data field. */ > + if (apdu->a_Lc == 0) { > + *response = vcard_make_response( > + VCARD7816_STATUS_ERROR_DATA_INVALID); > + ret = VCARD_DONE; > + break; > + } > + *response = get_properties(card, applet_private->properties, > + applet_private->properties_len, apdu->a_body, apdu->a_Lc, apdu->a_Le); > + break; > + default: > + /* unknown params returns (SW1=0x6A, SW2=0x86) */ > + *response = vcard_make_response( > + VCARD7816_STATUS_ERROR_P1_P2_INCORRECT); > + break; > + } > + ret = VCARD_DONE; > + break; > case VCARD7816_INS_SELECT_FILE: > if (apdu->a_p1 != 0x02) { > /* let the 7816 code handle applet switches */ > @@ -78,12 +262,6 @@ cac_common_process_apdu(VCard *card, VCardAPDU *apdu, VCardResponse **response) > /* let the 7816 code handle these */ > ret = VCARD_NEXT; > break; > - case CAC_GET_PROPERTIES: > - case CAC_GET_ACR: > - /* skip these for now, this will probably be needed */ > - *response = vcard_make_response(VCARD7816_STATUS_ERROR_P1_P2_INCORRECT); > - ret = VCARD_DONE; > - break; > default: > *response = vcard_make_response( > VCARD7816_STATUS_ERROR_COMMAND_NOT_SUPPORTED); > @@ -93,6 +271,87 @@ cac_common_process_apdu(VCard *card, VCardAPDU *apdu, VCardResponse **response) > return ret; > } > > +/* > + * Handle READ BUFFER APDU and other common APDUs for CAC applets > + */ > +static VCardStatus > +cac_common_process_apdu_read(VCard *card, VCardAPDU *apdu, > + VCardResponse **response) > +{ > + VCardAppletPrivate *applet_private; > + VCardStatus ret = VCARD_FAIL; > + int size, offset; > + > + applet_private = vcard_get_current_applet_private(card, apdu->a_channel); > + > + switch (apdu->a_ins) { > + case CAC_READ_BUFFER: > + /* Body contains exactly two bytes */ > + if (!apdu->a_body || apdu->a_Lc != 2) { > + *response = vcard_make_response( > + VCARD7816_STATUS_ERROR_DATA_INVALID); > + ret = VCARD_DONE; > + break; > + } > + > + /* Second byte defines how many bytes should be read */ > + size = apdu->a_body[1]; > + > + /* P1 | P2 defines offset to read from */ > + offset = (apdu->a_p1 << 8) | apdu->a_p2; > + g_debug("%s: Reqested offset: %d bytes", __func__, offset); > + > + /* First byte selects TAG+LEN or VALUE buffer */ > + switch (apdu->a_body[0]) { > + case CAC_FILE_VALUE: > + size = MIN(size, applet_private->val_buffer_len - offset); > + if (size < 0) { /* Overrun returns (SW1=0x6A, SW2=0x86) */ > + *response = vcard_make_response( > + VCARD7816_STATUS_ERROR_P1_P2_INCORRECT); > + break; > + } > + *response = vcard_response_new_bytes( > + card, applet_private->val_buffer + offset, size, > + apdu->a_Le, VCARD7816_SW1_SUCCESS, 0); > + break; > + case CAC_FILE_TAG: > + g_debug("%s: Reqested: %d bytes", __func__, size); > + size = MIN(size, applet_private->tag_buffer_len - offset); > + if (size < 0) { /* Overrun returns (SW1=0x6A, SW2=0x86) */ > + *response = vcard_make_response( > + VCARD7816_STATUS_ERROR_P1_P2_INCORRECT); > + break; > + } > + g_debug("%s: Returning: %d bytes (have %d)", __func__, size, > + applet_private->tag_buffer_len); > + *response = vcard_response_new_bytes( > + card, applet_private->tag_buffer + offset, size, > + apdu->a_Le, VCARD7816_SW1_SUCCESS, 0); > + break; > + default: > + *response = vcard_make_response( > + VCARD7816_STATUS_ERROR_DATA_INVALID); > + break; > + } > + if (*response == NULL) { > + *response = vcard_make_response( > + VCARD7816_STATUS_EXC_ERROR_MEMORY_FAILURE); > + } > + ret = VCARD_DONE; > + break; > + case CAC_UPDATE_BUFFER: > + *response = vcard_make_response( > + VCARD7816_STATUS_ERROR_COMMAND_NOT_SUPPORTED); > + ret = VCARD_DONE; > + break; > + default: > + ret = cac_common_process_apdu(card, apdu, response); > + break; > + } > + return ret; > +} > + > + > /* > * reset the inter call state between applet selects > */ > @@ -105,10 +364,8 @@ cac_applet_pki_reset(VCard *card, int channel) > assert(applet_private); > pki_applet = &(applet_private->u.pki_data); > > - pki_applet->cert_buffer = NULL; > g_free(pki_applet->sign_buffer); > pki_applet->sign_buffer = NULL; > - pki_applet->cert_buffer_len = 0; > pki_applet->sign_buffer_len = 0; > return VCARD_DONE; > } > @@ -119,7 +376,7 @@ cac_applet_pki_process_apdu(VCard *card, VCardAPDU *apdu, > { > CACPKIAppletData *pki_applet; > VCardAppletPrivate *applet_private; > - int size, next; > + int size; > unsigned char *sign_buffer; > bool retain_sign_buffer = FALSE; > vcard_7816_status_t status; > @@ -135,37 +392,6 @@ cac_applet_pki_process_apdu(VCard *card, VCardAPDU *apdu, > VCARD7816_STATUS_ERROR_CONDITION_NOT_SATISFIED); > ret = VCARD_DONE; > break; > - case CAC_GET_CERTIFICATE: > - if ((apdu->a_p2 != 0) || (apdu->a_p1 != 0)) { > - *response = vcard_make_response( > - VCARD7816_STATUS_ERROR_P1_P2_INCORRECT); > - break; > - } > - assert(pki_applet->cert != NULL); > - size = apdu->a_Le; > - if (pki_applet->cert_buffer == NULL) { > - pki_applet->cert_buffer = pki_applet->cert; > - pki_applet->cert_buffer_len = pki_applet->cert_len; > - } > - size = MIN(size, pki_applet->cert_buffer_len); > - next = MIN(255, pki_applet->cert_buffer_len - size); > - *response = vcard_response_new_bytes( > - card, pki_applet->cert_buffer, size, > - apdu->a_Le, next ? > - VCARD7816_SW1_WARNING_CHANGE : > - VCARD7816_SW1_SUCCESS, > - next); > - pki_applet->cert_buffer += size; > - pki_applet->cert_buffer_len -= size; > - if ((*response == NULL) || (next == 0)) { > - pki_applet->cert_buffer = NULL; > - } > - if (*response == NULL) { > - *response = vcard_make_response( > - VCARD7816_STATUS_EXC_ERROR_MEMORY_FAILURE); > - } > - ret = VCARD_DONE; > - break; > case CAC_SIGN_DECRYPT: > if (apdu->a_p2 != 0) { > *response = vcard_make_response( > @@ -215,21 +441,13 @@ cac_applet_pki_process_apdu(VCard *card, VCardAPDU *apdu, > } > ret = VCARD_DONE; > break; > - case CAC_READ_BUFFER: > - /* new CAC call, go ahead and use the old version for now */ > - /* TODO: implement */ > - *response = vcard_make_response( > - VCARD7816_STATUS_ERROR_COMMAND_NOT_SUPPORTED); > - ret = VCARD_DONE; > - break; > default: > - ret = cac_common_process_apdu(card, apdu, response); > + ret = cac_common_process_apdu_read(card, apdu, response); > break; > } > return ret; > } > > - > static VCardStatus > cac_applet_id_process_apdu(VCard *card, VCardAPDU *apdu, > VCardResponse **response) > @@ -293,8 +511,11 @@ cac_delete_pki_applet_private(VCardAppletPrivate *applet_private) > return; > } > pki_applet_data = &(applet_private->u.pki_data); > - g_free(pki_applet_data->cert); > g_free(pki_applet_data->sign_buffer); > + g_free(applet_private->tag_buffer); > + g_free(applet_private->val_buffer); > + /* this one is cloned so needs to be freed */ > + simpletlv_free(applet_private->properties, applet_private->properties_len); > if (pki_applet_data->key != NULL) { > vcard_emul_delete_key(pki_applet_data->key); > } > @@ -302,25 +523,149 @@ cac_delete_pki_applet_private(VCardAppletPrivate *applet_private) > } > > static VCardAppletPrivate * > -cac_new_pki_applet_private(const unsigned char *cert, > +cac_new_pki_applet_private(int i, const unsigned char *cert, > int cert_len, VCardKey *key) > { > CACPKIAppletData *pki_applet_data; > VCardAppletPrivate *applet_private; > > + /* PKI applet Properies ex.: > + * 01 Tag: Applet Information > + * 05 Length > + * 10 Applet family > + * 02 06 02 03 Applet version > + * 40 Tag: Number of objects managed by this instance > + * 01 Length > + * 01 One > + * 51 Tag: First PKI object > + * 11 Length > + * 41 Tag: ObjectID > + * 02 Length > + * 01 01 > + * 42 Buffer properties > + * 05 Length > + * 00 Type of tag supported > + * 1E 00 T-Buffer length (LSB, MSB) > + * 54 05 V-Buffer length (LSB, MSB) > + * 43 Tag: PKI properties > + * 04 Length > + * 06 Algorithm ID Table 5-6 in GSC-IS 2.1 > + * 10 Key length bytes /8 > + * 01 Private key initialized > + * 01 Public key initialized > + */ > + unsigned char object_id[] = "\x01\x00"; > + unsigned char buffer_properties[] = "\x00\x00\x00\x00\x00"; > + unsigned char pki_properties[] = "\x06\x10\x01\x01"; > + static struct simpletlv_member pki_object[3] = { > + {CAC_PROPERTIES_OBJECT_ID, 2, {/*.value = object_id*/}, > + SIMPLETLV_TYPE_LEAF}, > + {CAC_PROPERTIES_BUFFER_PROPERTIES, 5, {/*.value = buffer_properties*/}, > + SIMPLETLV_TYPE_LEAF}, > + {CAC_PROPERTIES_PKI_PROPERTIES, 4, {/*.value = pki_properties*/}, > + SIMPLETLV_TYPE_LEAF}, > + }; > + unsigned char applet_information[] = "\x10\x02\x06\x02\x03"; > + unsigned char number_objects[] = "\x01"; > + static struct simpletlv_member properties[4] = { > + {CAC_PROPERTIES_APPLET_INFORMATION, 5, {/*.value = applet_information*/}, > + SIMPLETLV_TYPE_LEAF}, > + {CAC_PROPERTIES_NUMBER_OBJECTS, 1, {/*.value = number_objects */}, > + SIMPLETLV_TYPE_LEAF}, > + {CAC_PROPERTIES_PKI_OBJECT, 3, {/*.child = pki_object*/}, > + SIMPLETLV_TYPE_COMPOUND}, > + }; > + /* if this would be 1, the certificate would be compressed */ > + unsigned char certinfo[] = "\x00"; > + struct simpletlv_member buffer[] = { > + {CAC_PKI_TAG_CERTINFO, 1, {/*.value = certinfo*/}, SIMPLETLV_TYPE_LEAF}, > + {CAC_PKI_TAG_CERTIFICATE, cert_len, {/*.value = cert*/}, SIMPLETLV_TYPE_LEAF}, > + }; > + > applet_private = g_new0(VCardAppletPrivate, 1); > pki_applet_data = &(applet_private->u.pki_data); > - pki_applet_data->cert = (unsigned char *)g_malloc(cert_len+1); > /* > * if we want to support compression, then we simply change the 0 to a 1 > - * and compress the cert data with libz > + * in certinfo and compress the cert data with libz > + */ > + > + /* prepare the buffers to when READ_BUFFER will be called. > + * Assuming VM card with (LSB first if > 255) > + * separate Tag+Length, Value buffers as described in 8.4: > + * 2 B 1 B 1-3 B 1 B 1-3 B > + * [ T-Len ] [ Tag1 ] [ Len1 ] [ Tag2] [ Len2 ] [...] > + * > + * 2 B Len1 B Len2 B > + * [ V-Len ] [ Value 1 ] [ Value 2 ] [...] > + * */ > + > + /* Tag+Len buffer */ > + buffer[0].value.value = certinfo; > + buffer[1].value.value = (unsigned char *)cert; > + /* Ex: > + * 0A 00 Length of whole buffer > + * 71 Tag: CertInfo > + * 01 Length: 1B > + * 70 Tag: Certificate > + * FF B2 03 Length: (\x03 << 8) || \xB2 > + * 72 Tag: MSCUID > + * 26 Length > + */ > + applet_private->tag_buffer_len = cac_create_tl_file(buffer, 4, > + &applet_private->tag_buffer); > + if (applet_private->tag_buffer_len == 0) > + goto failure; > + g_debug("%s: applet_private->tag_buffer = %s", __func__, > + hex_dump(applet_private->tag_buffer, applet_private->tag_buffer_len, NULL, 0)); > + > + /* Value buffer */ > + /* Ex: > + * DA 03 Length of complete buffer > + * 01 Value of CertInfo > + * 78 [..] 6C Cert Value > + * 7B 63 37 35 62 62 61 64 61 2D 35 32 39 38 2D 31 > + * 37 35 62 2D 39 32 64 63 2D 39 38 35 30 36 62 65 > + * 30 30 30 30 30 7D MSCUID Value > */ > - pki_applet_data->cert[0] = 0; /* not compressed */ > - memcpy(&pki_applet_data->cert[1], cert, cert_len); > - pki_applet_data->cert_len = cert_len+1; > + applet_private->val_buffer_len = cac_create_val_file(buffer, 4, > + &applet_private->val_buffer); > + if (applet_private->val_buffer_len == 0) > + goto failure; > + g_debug("%s: applet_private->val_buffer = %s", __func__, > + hex_dump(applet_private->val_buffer, applet_private->val_buffer_len, NULL, 0)); > + > + /* Inject Object ID */ > + object_id[1] = i; > + pki_object[0].value.value = object_id; > + > + /* Inject T-Buffer and V-Buffer lengths in the properties buffer */ > + ushort2lebytes(&buffer_properties[1], applet_private->tag_buffer_len); > + ushort2lebytes(&buffer_properties[3], applet_private->val_buffer_len); > + pki_object[1].value.value = buffer_properties; > + > + /* PKI properties needs adjustments based on the key sizes */ > + // TODO XXX assuming 1024 b keys! > + // pki_properties[1] = key_bits >> 3; > + pki_object[2].value.value = pki_properties; > + > + /* Inject Applet Version */ > + properties[0].value.value = applet_information; > + properties[1].value.value = number_objects; > + properties[2].value.child = pki_object; > + > + /* Clone the properties */ > + applet_private->properties_len = 4; > + applet_private->properties = simpletlv_clone(properties, 4); > + if (applet_private->properties == NULL) > + goto failure; > > pki_applet_data->key = key; > return applet_private; > + > +failure: > + if (applet_private) > + cac_delete_pki_applet_private(applet_private); > + return NULL; > } > > > @@ -338,7 +683,7 @@ cac_new_pki_applet(int i, const unsigned char *cert, > > pki_aid[pki_aid_len-1] = i; > > - applet_private = cac_new_pki_applet_private(cert, cert_len, key); > + applet_private = cac_new_pki_applet_private(i, cert, cert_len, key); > if (applet_private == NULL) { > goto failure; > } > diff --git a/src/cac.h b/src/cac.h > index af7f4cd..7c5e9a3 100644 > --- a/src/cac.h > +++ b/src/cac.h > @@ -11,12 +11,30 @@ > #include "vcard.h" > #include "vreader.h" > > -#define CAC_GET_PROPERTIES 0x56 > -#define CAC_GET_ACR 0x4c > -#define CAC_READ_BUFFER 0x52 > -#define CAC_UPDATE_BUFFER 0x58 > -#define CAC_SIGN_DECRYPT 0x42 > -#define CAC_GET_CERTIFICATE 0x36 > +#define CAC_GET_PROPERTIES 0x56 > +#define CAC_GET_ACR 0x4c > +#define CAC_READ_BUFFER 0x52 /* CACv2 */ > +#define CAC_UPDATE_BUFFER 0x58 > +#define CAC_SIGN_DECRYPT 0x42 > +#define CAC_GET_CERTIFICATE 0x36 /* CACv1 */ > + > +/* read file TAGs for CACv2 */ > +#define CAC_FILE_TAG 0x01 > +#define CAC_FILE_VALUE 0x02 > + > +/* PKI applet tags */ > +#define CAC_PKI_TAG_CERTIFICATE 0x70 > +#define CAC_PKI_TAG_CERTINFO 0x71 > + > +/* Applet properties tags */ > +#define CAC_PROPERTIES_APPLET_INFORMATION 0x01 > +#define CAC_PROPERTIES_NUMBER_OBJECTS 0x40 > +#define CAC_PROPERTIES_OBJECT_ID 0x41 > +#define CAC_PROPERTIES_BUFFER_PROPERTIES 0x42 > +#define CAC_PROPERTIES_PKI_PROPERTIES 0x43 > +#define CAC_PROPERTIES_TV_OBJECT 0x50 > +#define CAC_PROPERTIES_PKI_OBJECT 0x51 > + > > /* > * Initialize the cac card. This is the only public function in this file. All > -- > 2.17.1 > > _______________________________________________ > Spice-devel mailing list > Spice-devel@xxxxxxxxxxxxxxxxxxxxx > https://lists.freedesktop.org/mailman/listinfo/spice-devel -- Marc-André Lureau _______________________________________________ Spice-devel mailing list Spice-devel@xxxxxxxxxxxxxxxxxxxxx https://lists.freedesktop.org/mailman/listinfo/spice-devel