Tue, 29 Mar 2016 22:51:43 -0500
Add TLS Certificate parsing API
This patch adds X.509 certificate parsing API. It takes the bytes
from a GTlsCertificate and parses information such as subject name,
SHA-1 hash, and similar. GTlsCertificate parses the certificates
internally, so these functions are only used for displaying to the
user.
UIs could conceivably use a library such as libgcr directly instead,
but this is here, at least for now, until such an alternative is
used, if at all.
| libpurple/tls-certificate.c | file | annotate | diff | comparison | revisions | |
| libpurple/tls-certificate.h | file | annotate | diff | comparison | revisions |
--- a/libpurple/tls-certificate.c Tue Feb 16 17:48:03 2016 -0600 +++ b/libpurple/tls-certificate.c Tue Mar 29 22:51:43 2016 -0500 @@ -23,6 +23,7 @@ #include "internal.h" #include "tls-certificate.h" +#include "ciphers/sha1hash.h" #include "debug.h" #include "util.h" @@ -277,3 +278,753 @@ socket_client_event_cb, NULL, NULL); } +#define DER_TYPE_CLASS(type) (type & 0xc0) + +#define DER_TYPE_CLASS_UNIVERSAL 0x00 +#define DER_TYPE_CLASS_APPLICATION 0x40 +#define DER_TYPE_CLASS_CONTEXT_SPECIFIC 0x80 +#define DER_TYPE_CLASS_PRIVATE 0xc0 + +#define DER_TYPE_TAG(type) (type & 0x1f) + +#define DER_TYPE_IS_CONSTRUCTED(type) ((type & 0x20) ? TRUE : FALSE) + +#define DER_TYPE_TAG_IS_LONG_FORM(type) (DER_TYPE_TAG(type) == 0x1f) + +#define DER_LENGTH_IS_LONG_FORM(byte) ((byte & 0x80) ? TRUE : FALSE) +#define DER_LENGTH_LONG_FORM_SIZE(byte) (byte & 0x7f) + +typedef struct { + guint8 type_class; + gboolean constructed; + guint type; + GBytes *content; + GSList *children; +} DerNodeData; + +static void der_node_data_children_list_free(GSList *children); + +static void +der_node_data_free(DerNodeData *node_data) +{ + g_return_if_fail(node_data != NULL); + + g_clear_pointer(&node_data->content, g_bytes_unref); + g_clear_pointer(&node_data->children, + der_node_data_children_list_free); + + g_free(node_data); +} + +static void +der_node_data_children_list_free(GSList *children) +{ + g_return_if_fail(children != NULL); + + g_slist_free_full(children, (GDestroyNotify)der_node_data_free); +} + +/* Parses DER encoded data into a GSList of DerNodeData instances */ +static GSList * +der_parse(GBytes *data_bytes) +{ + const guint8 *data; + gsize size = 0; + gsize offset = 0; + GSList *nodes = NULL; + DerNodeData *node = NULL; + + data = g_bytes_get_data(data_bytes, &size); + + /* Parse data */ + while (offset < size) { + guint8 byte; + gsize length; + + /* Parse type */ + + byte = *(data + offset++); + node = g_new0(DerNodeData, 1); + node->type_class = DER_TYPE_CLASS(byte); + node->constructed = DER_TYPE_IS_CONSTRUCTED(byte); + + if (DER_TYPE_TAG_IS_LONG_FORM(byte)) { + /* Long-form type encoding */ + /* TODO: Handle long-form encoding. + * Maiku: The certificates I tested didn't do this. + */ + g_return_val_if_reached(NULL); + } else { + /* Short-form type encoding */ + node->type = DER_TYPE_TAG(byte); + } + + /* Parse content length */ + + if (offset >= size) { + purple_debug_error("tls-certificate", + "Not enough remaining data when " + "parsing DER chunk length byte: " + "read (%" G_GSIZE_FORMAT ") " + "available: ""(%" G_GSIZE_FORMAT ")", + offset, size); + break; + } + + byte = *(data + offset++); + + if (DER_LENGTH_IS_LONG_FORM(byte)) { + /* Long-form length encoding */ + guint num_len_bytes = DER_LENGTH_LONG_FORM_SIZE(byte); + guint i; + + /* Guard against overflowing the integer */ + if (num_len_bytes > sizeof(guint)) { + purple_debug_error("tls-certificate", + "Number of long-form length " + "bytes greater than guint " + "size: %u > %" G_GSIZE_FORMAT, + num_len_bytes, sizeof(guint)); + break; + } + + /* Guard against reading past the end of the buffer */ + if (offset + num_len_bytes > size) { + purple_debug_error("tls-certificate", + "Not enough remaining data " + "when parsing DER chunk " + "long-form length bytes: " + "read (%" G_GSIZE_FORMAT ") " + "available: ""(%" + G_GSIZE_FORMAT ")", + offset, size); + break; + } + + length = 0; + + for (i = 0; i < num_len_bytes; ++i) { + length = length << 8; + length |= *(data + offset++); + } + } else { + /* Short-form length encoding */ + length = byte; + } + + /* Parse content */ + + if (offset + length > size) { + purple_debug_error("tls-certificate", + "Not enough remaining data when " + "parsing DER chunk content: " + "content size (%" G_GSIZE_FORMAT ") " + "available: ""(%" G_GSIZE_FORMAT ")", + length, size - offset); + break; + } + + node->content = g_bytes_new_from_bytes(data_bytes, + offset, length); + offset += length; + + /* Maybe recurse */ + if (node->constructed) { + node->children = der_parse(node->content); + + if (node->children == NULL) { + /* No children on a constructed type + * should an error. If this happens, it + * outputs debug info inside der_parse(). + */ + break; + } + } + + nodes = g_slist_append(nodes, node); + node = NULL; + } + + if (node != NULL) { + /* There was an error. Free parsing data. */ + der_node_data_free(node); + g_clear_pointer(&nodes, der_node_data_children_list_free); + /* FIXME: Report error to calling function ala GError? */ + } + + return nodes; +} + +static gchar * +der_parse_string(DerNodeData *node) +{ + const gchar *str; + gsize length = 0; + + g_return_val_if_fail(node != NULL, NULL); + g_return_val_if_fail(node->content != NULL, NULL); + + str = g_bytes_get_data(node->content, &length); + return g_strndup(str, length); +} + +typedef struct { + gchar *oid; + gchar *value; +} DerOIDValue; + +static void +der_oid_value_free(DerOIDValue *data) +{ + g_return_if_fail(data != NULL); + + g_clear_pointer(&data->oid, g_free); + g_clear_pointer(&data->value, g_free); + + g_free(data); +} + +static void +der_oid_value_slist_free(GSList *list) +{ + g_return_if_fail(list != NULL); + + g_slist_free_full(list, (GDestroyNotify)der_oid_value_free); +} + +static const gchar * +der_oid_value_slist_get_value_by_oid(GSList *list, const gchar *oid) +{ + for (; list != NULL; list = g_slist_next(list)) { + DerOIDValue *value = list->data; + + if (!strcmp(oid, value->oid)) { + return value->value; + } + } + + return NULL; +} + +static gchar * +der_parse_oid(DerNodeData *node) +{ + const gchar *oid_data; + gsize length = 0; + gsize offset = 0; + guint8 byte; + GString *ret; + + g_return_val_if_fail(node != NULL, NULL); + g_return_val_if_fail(node->content != NULL, NULL); + + oid_data = g_bytes_get_data(node->content, &length); + /* Most OIDs used for certificates aren't larger than 9 bytes */ + ret = g_string_sized_new(9); + + /* First byte is encoded as num1 * 40 + num2 */ + if (length > 0) { + byte = *(oid_data + offset++); + g_string_append_printf(ret, "%u.%u", byte / 40, byte % 40); + } + + /* Subsequent numbers are in base 128 format (the most + * significant bit being set adds another 7 bits to the number) + */ + while (offset < length) { + guint value = 0; + + do { + byte = *(oid_data + offset++); + value = (value << 7) + (byte & 0x7f); + } while (byte & 0x80 && offset < length); + + g_string_append_printf(ret, ".%u", value); + } + + return g_string_free(ret, FALSE); +} + +/* Parses X.509 Issuer and Subject name structures + * into a GSList of DerOIDValue. + */ +static GSList * +der_parse_name(DerNodeData *name_node) +{ + GSList *list; + GSList *ret = NULL; + DerOIDValue *value; + + g_return_val_if_fail(name_node != NULL, NULL); + + /* Iterate over items in the name sequence */ + list = name_node->children; + + while (list != NULL) { + DerNodeData *child_node; + GSList *child_list; + + value = g_new(DerOIDValue, 1); + + /* Each item in the name sequence is a set containing + * a sequence of an ObjectID and a String-like value + */ + + /* Get the DerNode containing set data */ + if ((child_node = g_slist_nth_data(list, 0)) == NULL) { + break; + } + + /* Get the DerNode containing its sequence data */ + if (child_node == NULL || + (child_node = g_slist_nth_data( + child_node->children, 0)) == NULL) { + break; + } + + /* Get the GSList item containing the ObjectID DerNode */ + if ((child_list = child_node->children) == NULL) { + break; + } + + /* Get the DerNode containing the ObjectID */ + if ((child_node = child_list->data) == NULL) { + break; + } + + /* Parse ObjectID */ + value->oid = der_parse_oid(child_node); + + /* Get the GSList item containing the String-like value */ + if ((child_list = g_slist_next(child_list)) == NULL) { + break; + } + + /* Get the DerNode containing the String-like value */ + if ((child_node = child_list->data) == NULL) { + break; + } + + /* Parse String-like value */ + value->value = der_parse_string(child_node); + + ret = g_slist_prepend(ret, value); + list = g_slist_next(list); + value = NULL; + } + + if (value != NULL) { + der_oid_value_free(value); + der_oid_value_slist_free(ret); + } + + return g_slist_reverse(ret); +} + +static GDateTime * +der_parse_time(DerNodeData *node) +{ + gchar *time; + gchar *c; + gint time_parts[7]; + gint time_part_idx = 0; + int length; + + g_return_val_if_fail(node != NULL, NULL); + g_return_val_if_fail(node->content != NULL, NULL); + + time = der_parse_string(node); + + /* For the purposes of X.509 + * UTCTime format is "YYMMDDhhmmssZ" (YY >= 50 ? 19YY : 20YY) and + * GeneralizedTime format is "YYYYMMDDhhmmssZ" + * According to RFC2459, they both are GMT, which is weird + * considering one is named UTC, but for the purposes of display, + * for which this is used, it shouldn't matter. + */ + + length = strlen(time); + if (length == 13) { + /* UTCTime: Skip the first part as it's calculated later */ + time_part_idx = 1; + } else if (length == 15) { + /* Generalized Time */ + /* TODO: Handle generalized time + * Maiku: None of the certificates I tested used this + */ + g_free(time); + g_return_val_if_reached(NULL); + } else { + purple_debug_error("tls-certificate", + "Unrecognized time format (length: %i)", + length); + g_free(time); + return NULL; + } + + c = time; + + while (c - time < length) { + if (*c == 'Z') { + break; + } + + if (!g_ascii_isdigit(*c) || !g_ascii_isdigit(*(c + 1))) { + purple_debug_error("tls-certificate", + "Error parsing time. next characters " + "aren't both digits: '%c%c'", + *c, *(c + 1)); + break; + } + + time_parts[time_part_idx++] = + g_ascii_digit_value(*c) * 10 + + g_ascii_digit_value(*(c + 1)); + c += 2; + } + + if (length == 13) { + if (time_parts[1] >= 50) { + time_parts[0] = 19; + } else { + time_parts[0] = 20; + } + } + + return g_date_time_new_utc( + time_parts[0] * 100 + time_parts[1], /* year */ + time_parts[2], /* month */ + time_parts[3], /* day */ + time_parts[4], /* hour */ + time_parts[5], /* minute */ + time_parts[6]); /* seconds */ +} + +/* This structure contains the data which is in an X.509 certificate. + * Only the values actually parsed/used are here. The remaining commented + * out values are informative placeholders for the remaining data that + * could be in a standard certificate. + */ +struct _PurpleTlsCertificateInfo { + GTlsCertificate *cert; + + /* version (Optional, defaults to version 1 (version = value + 1)) */ + /* serialNumber */ + /* signature */ + GSList *issuer; + GDateTime *notBefore; + GDateTime *notAfter; + GSList *subject; + /* subjectPublicKeyInfo */ + /* issuerUniqueIdentifier (Optional, requires version 2 or 3) */ + /* subjectUniqueIdentifier (Optional, requires version 2 or 3) */ + /* extensions (Optional, requires version 3) */ +}; + +/* TODO: Make better API for this? */ +PurpleTlsCertificateInfo * +purple_tls_certificate_get_info(GTlsCertificate *certificate) +{ + GByteArray *der_array = NULL; + GBytes *root; + GSList *nodes; + DerNodeData *node; + DerNodeData *cert_node; + DerNodeData *valid_node; + PurpleTlsCertificateInfo *info; + + g_return_val_if_fail(G_IS_TLS_CERTIFICATE(certificate), NULL); + + /* Get raw bytes from DER formatted certificate */ + g_object_get(certificate, "certificate", &der_array, NULL); + + /* Parse raw bytes into DerNode tree */ + root = g_byte_array_free_to_bytes(der_array); + nodes = der_parse(root); + g_bytes_unref(root); + + if (nodes == NULL) { + purple_debug_warning("tls-certificate", + "Error parsing certificate"); + return NULL; + } + + /* Set up PurpleTlsCertificateInfo struct with initial data */ + info = g_new0(PurpleTlsCertificateInfo, 1); + info->cert = g_object_ref(certificate); + + /* Get certificate root sequence GSList item */ + node = g_slist_nth_data(nodes, 0); + if (node == NULL || node->children == NULL) { + purple_debug_warning("tls-certificate", + "Error parsing certificate root node"); + purple_tls_certificate_info_free(info); + return NULL; + } + + /* Get certificate sequence GSList DerNode */ + cert_node = g_slist_nth_data(node->children, 0); + if (cert_node == NULL || cert_node->children == NULL) { + purple_debug_warning("tls-certificate", + "Error to parsing certificate node"); + purple_tls_certificate_info_free(info); + return NULL; + } + + /* Check for optional certificate version */ + + node = g_slist_nth_data(cert_node->children, 0); + if (node == NULL || node->children == NULL) { + purple_debug_warning("tls-certificate", + "Error to parsing certificate version node"); + purple_tls_certificate_info_free(info); + return NULL; + } + + if (node->type_class != DER_TYPE_CLASS_CONTEXT_SPECIFIC) { + /* Include optional version so indices work right */ + /* TODO: Actually set default version value? */ + cert_node->children = + g_slist_prepend(cert_node->children, NULL); + } + + /* Get certificate issuer */ + + node = g_slist_nth_data(cert_node->children, 3); + if (node == NULL || node->children == NULL) { + purple_debug_warning("tls-certificate", + "Error to parsing certificate issuer node"); + purple_tls_certificate_info_free(info); + return NULL; + } + + info->issuer = der_parse_name(node); + + /* Get certificate validity */ + + valid_node = g_slist_nth_data(cert_node->children, 4); + if (valid_node == NULL || valid_node->children == NULL) { + purple_debug_warning("tls-certificate", + "Error to parsing certificate validity node"); + purple_tls_certificate_info_free(info); + return NULL; + } + + /* Get certificate validity (notBefore) */ + node = g_slist_nth_data(valid_node->children, 0); + if (node == NULL) { + purple_debug_warning("tls-certificate", + "Error to parsing certificate valid " + "notBefore node"); + purple_tls_certificate_info_free(info); + return NULL; + } + + info->notBefore = der_parse_time(node); + + /* Get certificate validity (notAfter) */ + node = g_slist_nth_data(valid_node->children, 1); + if (node == NULL) { + purple_debug_warning("tls-certificate", + "Error to parsing certificate valid " + "notAfter node"); + purple_tls_certificate_info_free(info); + return NULL; + } + + info->notAfter = der_parse_time(node); + + /* Get certificate subject */ + + node = g_slist_nth_data(cert_node->children, 5); + if (node == NULL || node->children == NULL) { + purple_debug_warning("tls-certificate", + "Error to parsing certificate subject node"); + purple_tls_certificate_info_free(info); + return NULL; + } + + info->subject = der_parse_name(node); + + /* Clean up */ + der_node_data_children_list_free(nodes); + + return info; +} + +void +purple_tls_certificate_info_free(PurpleTlsCertificateInfo *info) +{ + g_return_if_fail(info != NULL); + + g_clear_object(&info->cert); + + g_clear_pointer(&info->issuer, der_oid_value_slist_free); + g_clear_pointer(&info->notBefore, g_date_time_unref); + g_clear_pointer(&info->notAfter, g_date_time_unref); + g_clear_pointer(&info->subject, der_oid_value_slist_free); + + g_free(info); +} + +/* Looks up the relative distinguished name (RDN) from an ObjectID */ +static const gchar * +lookup_rdn_name_by_oid(const gchar *oid) +{ + static GHashTable *ht = NULL; + + if (G_UNLIKELY(ht == NULL)) { + ht = g_hash_table_new_full(g_str_hash, g_str_equal, + NULL, NULL); + + /* commonName */ + g_hash_table_insert(ht, "2.5.4.3", "CN"); + /* countryName */ + g_hash_table_insert(ht, "2.5.4.6", "C"); + /* localityName */ + g_hash_table_insert(ht, "2.5.4.7", "L"); + /* stateOrProvinceName */ + g_hash_table_insert(ht, "2.5.4.8", "ST"); + /* organizationName */ + g_hash_table_insert(ht, "2.5.4.10", "O"); + /* organizationalUnitName */ + g_hash_table_insert(ht, "2.5.4.11", "OU"); + } + + return g_hash_table_lookup(ht, oid); +} + +/* Makes a distinguished name (DN) from + * a list of relative distinguished names (RDN). + * Order matters. + */ +static gchar * +make_dn_from_oid_value_slist(GSList *list) +{ + GString *str = g_string_new(NULL); + + for (; list != NULL; list = g_slist_next(list)) { + DerOIDValue *value = list->data; + const gchar *name; + gchar *new_value; + + if (value == NULL) { + purple_debug_error("tls-certificate", + "DerOIDValue data missing from GSList"); + continue; + } + + name = lookup_rdn_name_by_oid(value->oid); + /* Escape commas in value as that's the DN separator */ + new_value = purple_strreplace(value->value, ",", "\\,"); + g_string_append_printf(str, "%s=%s,", name, new_value); + g_free(new_value); + } + + /* Remove trailing comma */ + g_string_truncate(str, str->len - 1); + + return g_string_free(str, FALSE); +} + +static gchar * +purple_tls_certificate_info_get_issuer_dn(PurpleTlsCertificateInfo *info) +{ + g_return_val_if_fail(info != NULL, NULL); + g_return_val_if_fail(info->issuer != NULL, NULL); + + return make_dn_from_oid_value_slist(info->issuer); +} + +gchar * +purple_tls_certificate_info_get_display_string(PurpleTlsCertificateInfo *info) +{ + gchar *subject_name; + gchar *issuer_name = NULL; + GByteArray *sha1_bytes; + gchar *sha1_str = NULL; + gchar *activation_time; + gchar *expiration_time; + gchar *ret; + + g_return_val_if_fail(info != NULL, NULL); + + /* Getting the commonName of a CA supposedly doesn't work, but we + * shouldn't be dealing with those here anyway. + */ + subject_name = purple_tls_certificate_info_get_subject_name(info); + + issuer_name = purple_tls_certificate_info_get_issuer_dn(info); + + sha1_bytes = purple_tls_certificate_get_fingerprint_sha1(info->cert); + if (sha1_bytes != NULL) { + sha1_str = purple_base16_encode_chunked(sha1_bytes->data, + sha1_bytes->len); + g_byte_array_unref(sha1_bytes); + } + + activation_time = g_date_time_format(info->notBefore, "%c"); + expiration_time = g_date_time_format(info->notAfter, "%c"); + + ret = g_strdup_printf( + _("Common name: %s\n\n" + "Issued by: %s\n\n" + "Fingerprint (SHA1): %s\n\n" + "Activation date: %s\n" + "Expiriation date: %s\n"), + subject_name, + issuer_name, + sha1_str, + activation_time, + expiration_time); + + g_free(subject_name); + g_free(issuer_name); + g_free(sha1_str); + g_free(activation_time); + g_free(expiration_time); + + return ret; +} + +/* TODO: Make better API for this? */ +gchar * +purple_tls_certificate_info_get_subject_name(PurpleTlsCertificateInfo *info) +{ + g_return_val_if_fail(info != NULL, NULL); + g_return_val_if_fail(info->subject != NULL, NULL); + + /* commonName component of the subject */ + return g_strdup(der_oid_value_slist_get_value_by_oid(info->subject, + "2.5.4.3")); +} + +/* TODO: Make better API for this? */ +GByteArray * +purple_tls_certificate_get_fingerprint_sha1(GTlsCertificate *certificate) +{ + PurpleHash *hash; + GByteArray *der = NULL; + guint8 *data = NULL; + gsize buf_size = 0; + + g_return_val_if_fail(G_IS_TLS_CERTIFICATE(certificate), NULL); + + g_object_get(certificate, "certificate", &der, NULL); + + g_return_val_if_fail(der != NULL, NULL); + + hash = purple_sha1_hash_new(); + + buf_size = purple_hash_get_digest_size(hash); + data = g_malloc(buf_size); + + purple_hash_append(hash, der->data, der->len); + g_byte_array_unref(der); + + purple_hash_digest(hash, data, buf_size); + g_object_unref(hash); + + return g_byte_array_new_take(data, buf_size); +} +
--- a/libpurple/tls-certificate.h Tue Feb 16 17:48:03 2016 -0600 +++ b/libpurple/tls-certificate.h Tue Mar 29 22:51:43 2016 -0500 @@ -118,6 +118,70 @@ gpointer purple_tls_certificate_attach_to_socket_client(GSocketClient *client); + +/** + * PurpleTlsCertificateInfo + * + * An opaque structure to contain parsed certificate info, which + * can subsequently be accessed by purple_tls_certificate_info_* + * functions. + */ +typedef struct _PurpleTlsCertificateInfo PurpleTlsCertificateInfo; + +/** + * purple_tls_certificate_get_info: + * @certificate: Certificate from which to parse the info + * + * Returns a #PurpleTlsCertificateInfo containing parsed information + * of the certificate. + * + * Returns: #PurpleTlsCertificateInfo parsed from the certificate + */ +PurpleTlsCertificateInfo * +purple_tls_certificate_get_info(GTlsCertificate *certificate); + +/** + * purple_tls_certificate_info_free: + * @info: #PurpleTlsCertificateInfo object to free + * + * Frees @info. + */ +void +purple_tls_certificate_info_free(PurpleTlsCertificateInfo *info); + +/** + * purple_tls_certificate_info_get_display_string: + * @info: #PurpleTlsCertificateInfo from which to generate a display string + * + * Generates a user readable string to display information from @info + * + * Returns: A user readable string suitable to display to the user + */ +gchar * +purple_tls_certificate_info_get_display_string(PurpleTlsCertificateInfo *info); + +/** + * purple_tls_certificate_get_subject_name: + * @certificate: Certificate from which to get the subject name + * + * Returns the common subject name of the cert + * + * Returns: The subject name of the cert + */ +gchar * +purple_tls_certificate_info_get_subject_name(PurpleTlsCertificateInfo *info); + +/** + * purple_tls_certificate_get_fingerprint_sha1: + * @certificate: Certificate from which to get the SHA1 fingerprint + * + * Returns the SHA1 fingerprint of the cert + * + * Returns: The SHA1 fingerprint of the cert + */ +GByteArray * +purple_tls_certificate_get_fingerprint_sha1(GTlsCertificate *certificate); + G_END_DECLS #endif /* _PURPLE_TLS_CERTIFICATE_H */