* Purple is the legal property of its developers, whose names are too numerous * to list here. Please refer to the COPYRIGHT file distributed with this * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * This program 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 General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA #include "tls-certificate-info.h" #include "ciphers/sha1hash.h" #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) static void der_node_data_children_list_free(GSList *children); 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); 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 */ der_parse(GBytes *data_bytes) DerNodeData *node = NULL; data = g_bytes_get_data(data_bytes, &size); 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); /* Short-form type encoding */ node->type = DER_TYPE_TAG(byte); /* Parse content length */ purple_debug_error("tls-certificate", "Not enough remaining data when " "parsing DER chunk length byte: " "read (%" G_GSIZE_FORMAT ") " "available: ""(%" G_GSIZE_FORMAT ")", byte = *(data + offset++); if (DER_LENGTH_IS_LONG_FORM(byte)) { /* Long-form length encoding */ guint num_len_bytes = DER_LENGTH_LONG_FORM_SIZE(byte); /* 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)); /* 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 ") " for (i = 0; i < num_len_bytes; ++i) { length |= *(data + offset++); /* Short-form length encoding */ 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 ")", node->content = g_bytes_new_from_bytes(data_bytes, 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(). nodes = g_slist_append(nodes, node); /* 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? */ der_parse_string(DerNodeData *node) 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); der_oid_value_copy(DerOIDValue *data) g_return_val_if_fail(data != NULL, NULL); ret = g_new0(DerOIDValue, 1); ret->oid = g_strdup(data->oid); ret->value = g_strdup(data->value); 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); der_oid_value_slist_free(GSList *list) g_return_if_fail(list != NULL); g_slist_free_full(list, (GDestroyNotify)der_oid_value_free); 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)) { der_parse_oid(DerNodeData *node) 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 */ 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) { 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. der_parse_name(DerNodeData *name_node) DerOIDValue *value = NULL; g_return_val_if_fail(name_node != NULL, NULL); /* Iterate over items in the name sequence */ list = name_node->children; 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) { /* Get the DerNode containing its sequence data */ if (child_node == NULL || (child_node = g_slist_nth_data( child_node->children, 0)) == NULL) { /* Get the GSList item containing the ObjectID DerNode */ if ((child_list = child_node->children) == NULL) { /* Get the DerNode containing the ObjectID */ if ((child_node = child_list->data) == NULL) { 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) { /* Get the DerNode containing the String-like value */ if ((child_node = child_list->data) == NULL) { /* Parse String-like value */ value->value = der_parse_string(child_node); ret = g_slist_prepend(ret, value); list = g_slist_next(list); der_oid_value_free(value); der_oid_value_slist_free(ret); return g_slist_reverse(ret); der_parse_time(DerNodeData *node) g_return_val_if_fail(node != NULL, NULL); g_return_val_if_fail(node->content != NULL, NULL); memset(time_parts, 0, sizeof(time_parts)); 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. /* UTCTime: Skip the first part as it's calculated later */ } else if (length == 15) { /* TODO: Handle generalized time * Maiku: None of the certificates I tested used this g_return_val_if_reached(NULL); purple_debug_error("tls-certificate", "Unrecognized time format (length: %i)", while (c - time < length) { 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'", time_parts[time_part_idx++] = g_ascii_digit_value(*c) * 10 + g_ascii_digit_value(*(c + 1)); if (time_parts[1] >= 50) { return g_date_time_new_utc( time_parts[0] * 100 + time_parts[1], /* year */ time_parts[2], /* month */ 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 { /* version (Optional, defaults to version 1 (version = value + 1)) */ /* 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; 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); purple_debug_warning("tls-certificate", "Error parsing certificate"); /* 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); /* 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); /* 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); if (node->type_class != DER_TYPE_CLASS_CONTEXT_SPECIFIC) { /* Include optional version so indices work right */ /* TODO: Actually set default version value? */ 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); 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); /* Get certificate validity (notBefore) */ node = g_slist_nth_data(valid_node->children, 0); purple_debug_warning("tls-certificate", "Error to parsing certificate valid " purple_tls_certificate_info_free(info); info->notBefore = der_parse_time(node); /* Get certificate validity (notAfter) */ node = g_slist_nth_data(valid_node->children, 1); purple_debug_warning("tls-certificate", "Error to parsing certificate valid " purple_tls_certificate_info_free(info); 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); info->subject = der_parse_name(node); der_node_data_children_list_free(nodes); static PurpleTlsCertificateInfo * purple_tls_certificate_info_copy(PurpleTlsCertificateInfo *info) PurpleTlsCertificateInfo *ret; g_return_val_if_fail(info != NULL, NULL); ret = g_new0(PurpleTlsCertificateInfo, 1); ret->issuer = g_slist_copy_deep(info->issuer, (GCopyFunc)der_oid_value_copy, NULL); ret->notBefore = g_date_time_ref(info->notBefore); ret->notAfter = g_date_time_ref(info->notAfter); ret->subject = g_slist_copy_deep(info->subject, (GCopyFunc)der_oid_value_copy, NULL); 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_DEFINE_BOXED_TYPE(PurpleTlsCertificateInfo, purple_tls_certificate_info, purple_tls_certificate_info_copy, purple_tls_certificate_info_free); /* Looks up the relative distinguished name (RDN) from an ObjectID */ 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, g_hash_table_insert(ht, "2.5.4.3", "CN"); g_hash_table_insert(ht, "2.5.4.6", "C"); g_hash_table_insert(ht, "2.5.4.7", "L"); /* stateOrProvinceName */ g_hash_table_insert(ht, "2.5.4.8", "ST"); 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). 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; purple_debug_error("tls-certificate", "DerOIDValue data missing from GSList"); 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); /* Remove trailing comma */ g_string_truncate(str, str->len - 1); return g_string_free(str, FALSE); 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); purple_tls_certificate_info_get_display_string(PurpleTlsCertificateInfo *info) gchar *issuer_name = NULL; 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, g_byte_array_unref(sha1_bytes); activation_time = g_date_time_format(info->notBefore, "%c"); expiration_time = g_date_time_format(info->notAfter, "%c"); "Fingerprint (SHA1): %s\n\n" "Expiriation date: %s\n"), /* TODO: Make better API for this? */ 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, /* TODO: Make better API for this? */ purple_tls_certificate_get_fingerprint_sha1(GTlsCertificate *certificate) 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); purple_hash_digest(hash, data, buf_size); return g_byte_array_new_take(data, buf_size);