/* * gnome-keyring * * Copyright (C) 2011 Collabora Ltd. * * This program 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 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, see . * * Author: Stef Walter */ #include "config.h" #include "gcr-openssh.h" #include "gcr-internal.h" #include "gcr-types.h" #include "gcr/gcr-oids.h" #include "egg/egg-asn1x.h" #include "egg/egg-asn1-defs.h" #include "egg/egg-buffer.h" #include "egg/egg-decimal.h" #include #include typedef struct { GcrOpensshPubCallback callback; gpointer user_data; } OpensshPubClosure; static void skip_spaces (const gchar ** line, gsize *n_line) { while (*n_line > 0 && (*line)[0] == ' ') { (*line)++; (*n_line)--; } } static gboolean next_word (const gchar **line, gsize *n_line, const gchar **word, gsize *n_word) { const gchar *beg; const gchar *end; const gchar *at; gboolean quotes; skip_spaces (line, n_line); if (!*n_line) { *word = NULL; *n_word = 0; return FALSE; } beg = at = *line; end = beg + *n_line; quotes = FALSE; do { switch (*at) { case '"': quotes = !quotes; at++; break; case ' ': if (!quotes) end = at; else at++; break; default: at++; break; } } while (at < end); *word = beg; *n_word = end - beg; (*line) += *n_word; (*n_line) -= *n_word; return TRUE; } static gboolean match_word (const gchar *word, gsize n_word, const gchar *matches) { gsize len = strlen (matches); if (len != n_word) return FALSE; return memcmp (word, matches, n_word) == 0; } static gulong keytype_to_algo (const gchar *algo, gsize length) { if (!algo) return G_MAXULONG; else if (match_word (algo, length, "ssh-rsa")) return CKK_RSA; else if (match_word (algo, length, "ssh-dss")) return CKK_DSA; else if (length >= 6 && strncmp (algo, "ecdsa-", 6) == 0) return CKK_ECDSA; return G_MAXULONG; } static gboolean read_decimal_mpi (const gchar *decimal, gsize n_decimal, GckBuilder *builder, gulong attribute_type) { gpointer data; gsize n_data; data = egg_decimal_decode (decimal, n_decimal, &n_data); if (data == NULL) return FALSE; gck_builder_add_data (builder, attribute_type, data, n_data); g_free (data); return TRUE; } static gint atoin (const char *p, gint digits) { gint ret = 0, base = 1; while(--digits >= 0) { if (p[digits] < '0' || p[digits] > '9') return -1; ret += (p[digits] - '0') * base; base *= 10; } return ret; } static GcrDataError parse_v1_public_line (const gchar *line, gsize length, GBytes *backing, GcrOpensshPubCallback callback, gpointer user_data) { const gchar *word_bits, *word_exponent, *word_modulus, *word_options, *outer; gsize len_bits, len_exponent, len_modulus, len_options, n_outer; GckBuilder builder = GCK_BUILDER_INIT; GckAttributes *attrs; gchar *label, *options; GBytes *bytes; gint bits; g_assert (line); outer = line; n_outer = length; options = NULL; label = NULL; /* Eat space at the front */ skip_spaces (&line, &length); /* Blank line or comment */ if (length == 0 || line[0] == '#') return GCR_ERROR_UNRECOGNIZED; /* * If the line starts with a digit, then no options: * * 2048 35 25213680043....93533757 Label * * If the line doesn't start with a digit, then have options: * * option,option 2048 35 25213680043....93533757 Label */ if (g_ascii_isdigit (line[0])) { word_options = NULL; len_options = 0; } else { if (!next_word (&line, &length, &word_options, &len_options)) return GCR_ERROR_UNRECOGNIZED; } if (!next_word (&line, &length, &word_bits, &len_bits) || !next_word (&line, &length, &word_exponent, &len_exponent) || !next_word (&line, &length, &word_modulus, &len_modulus)) return GCR_ERROR_UNRECOGNIZED; bits = atoin (word_bits, len_bits); if (bits <= 0) return GCR_ERROR_UNRECOGNIZED; if (!read_decimal_mpi (word_exponent, len_exponent, &builder, CKA_PUBLIC_EXPONENT) || !read_decimal_mpi (word_modulus, len_modulus, &builder, CKA_MODULUS)) { gck_builder_clear (&builder); return GCR_ERROR_UNRECOGNIZED; } gck_builder_add_ulong (&builder, CKA_KEY_TYPE, CKK_RSA); gck_builder_add_ulong (&builder, CKA_CLASS, CKO_PUBLIC_KEY); skip_spaces (&line, &length); if (length > 0) { label = g_strndup (line, length); g_strstrip (label); gck_builder_add_string (&builder, CKA_LABEL, label); } if (word_options) options = g_strndup (word_options, len_options); attrs = gck_builder_end (&builder); if (callback != NULL) { bytes = g_bytes_new_with_free_func (outer, n_outer, (GDestroyNotify)g_bytes_unref, g_bytes_ref (backing)); (callback) (attrs, label, options, bytes, user_data); g_bytes_unref (bytes); } gck_attributes_unref (attrs); g_free (options); g_free (label); return GCR_SUCCESS; } static gboolean read_buffer_mpi_to_der (EggBuffer *buffer, gsize *offset, GckBuilder *builder, gulong attribute_type) { const guchar *data, *data_value; GBytes *der_data = NULL; gsize len, data_len; GNode *asn = NULL; gboolean rv = FALSE; if (!egg_buffer_get_byte_array (buffer, *offset, offset, &data, &len)) return FALSE; asn = egg_asn1x_create (pk_asn1_tab, "ECPoint"); if (!asn) return FALSE; egg_asn1x_set_string_as_raw (asn, (guchar *)data, len, NULL); der_data = egg_asn1x_encode (asn, g_realloc); if (!der_data) goto out; data_value = g_bytes_get_data (der_data, &data_len); gck_builder_add_data (builder, attribute_type, data_value, data_len); rv = TRUE; out: g_bytes_unref (der_data); egg_asn1x_destroy (asn); return rv; } static gboolean read_buffer_mpi (EggBuffer *buffer, gsize *offset, GckBuilder *builder, gulong attribute_type) { const guchar *data; gsize len; if (!egg_buffer_get_byte_array (buffer, *offset, offset, &data, &len)) return FALSE; gck_builder_add_data (builder, attribute_type, data, len); return TRUE; } static gboolean read_v2_public_dsa (EggBuffer *buffer, gsize *offset, GckBuilder *builder) { if (!read_buffer_mpi (buffer, offset, builder, CKA_PRIME) || !read_buffer_mpi (buffer, offset, builder, CKA_SUBPRIME) || !read_buffer_mpi (buffer, offset, builder, CKA_BASE) || !read_buffer_mpi (buffer, offset, builder, CKA_VALUE)) { return FALSE; } gck_builder_add_ulong (builder, CKA_KEY_TYPE, CKK_DSA); gck_builder_add_ulong (builder, CKA_CLASS, CKO_PUBLIC_KEY); return TRUE; } static gboolean read_v2_public_rsa (EggBuffer *buffer, gsize *offset, GckBuilder *builder) { if (!read_buffer_mpi (buffer, offset, builder, CKA_PUBLIC_EXPONENT) || !read_buffer_mpi (buffer, offset, builder, CKA_MODULUS)) { return FALSE; } gck_builder_add_ulong (builder, CKA_KEY_TYPE, CKK_RSA); gck_builder_add_ulong (builder, CKA_CLASS, CKO_PUBLIC_KEY); return TRUE; } static gboolean read_v2_public_ecdsa (EggBuffer *buffer, gsize *offset, GckBuilder *builder) { gconstpointer data; GBytes *bytes; GNode *asn; GNode *node; gchar *curve; GQuark oid; gsize len; /* The named curve */ if (!egg_buffer_get_string (buffer, *offset, offset, &curve, (EggBufferAllocator)g_realloc)) return FALSE; if (g_strcmp0 (curve, "nistp256") == 0) { oid = GCR_OID_EC_SECP256R1; } else if (g_strcmp0 (curve, "nistp384") == 0) { oid = GCR_OID_EC_SECP384R1; } else if (g_strcmp0 (curve, "nistp521") == 0) { oid = GCR_OID_EC_SECP521R1; } else { g_free (curve); g_message ("unknown or unsupported curve in ssh public key"); return FALSE; } g_free (curve); asn = egg_asn1x_create (pk_asn1_tab, "ECParameters"); g_return_val_if_fail (asn != NULL, FALSE); node = egg_asn1x_node (asn, "namedCurve", NULL); if (!egg_asn1x_set_choice (asn, node)) g_return_val_if_reached (FALSE); if (!egg_asn1x_set_oid_as_quark (node, oid)) g_return_val_if_reached (FALSE); bytes = egg_asn1x_encode (asn, g_realloc); g_return_val_if_fail (bytes != NULL, FALSE); egg_asn1x_destroy (asn); data = g_bytes_get_data (bytes, &len); gck_builder_add_data (builder, CKA_EC_PARAMS, data, len); g_bytes_unref (bytes); /* need to convert to DER encoded OCTET STRING */ if (!read_buffer_mpi_to_der (buffer, offset, builder, CKA_EC_POINT)) return FALSE; gck_builder_add_ulong (builder, CKA_KEY_TYPE, CKK_ECDSA); gck_builder_add_ulong (builder, CKA_CLASS, CKO_PUBLIC_KEY); return TRUE; } static gboolean read_v2_public_key (gulong algo, gconstpointer data, gsize n_data, GckBuilder *builder) { EggBuffer buffer; gboolean ret; gsize offset; gchar *stype; int alg; egg_buffer_init_static (&buffer, data, n_data); offset = 0; /* The string algorithm */ if (!egg_buffer_get_string (&buffer, offset, &offset, &stype, (EggBufferAllocator)g_realloc)) return FALSE; alg = keytype_to_algo (stype, stype ? strlen (stype) : 0); g_free (stype); if (alg != algo) { g_message ("invalid or mis-matched algorithm in ssh public key: %s", stype); egg_buffer_uninit (&buffer); return FALSE; } switch (algo) { case CKK_RSA: ret = read_v2_public_rsa (&buffer, &offset, builder); break; case CKK_DSA: ret = read_v2_public_dsa (&buffer, &offset, builder); break; case CKK_ECDSA: ret = read_v2_public_ecdsa (&buffer, &offset, builder); break; default: g_assert_not_reached (); break; } egg_buffer_uninit (&buffer); return ret; } static gboolean decode_v2_public_key (gulong algo, const gchar *data, gsize n_data, GckBuilder *builder) { gpointer decoded; gsize n_decoded; gboolean ret; guint save; gint state; /* Decode the base64 key */ save = state = 0; decoded = g_malloc (n_data * 3 / 4); n_decoded = g_base64_decode_step ((gchar*)data, n_data, decoded, &state, &save); if (!n_decoded) { g_free (decoded); return FALSE; } /* Parse the actual key */ ret = read_v2_public_key (algo, decoded, n_decoded, builder); g_free (decoded); return ret; } static GcrDataError parse_v2_public_line (const gchar *line, gsize length, GBytes *backing, GcrOpensshPubCallback callback, gpointer user_data) { const gchar *word_options, *word_algo, *word_key; gsize len_options, len_algo, len_key; GckBuilder builder = GCK_BUILDER_INIT; GckAttributes *attrs; gchar *options; gchar *label = NULL; const gchar *outer = line; gsize n_outer = length; GBytes *bytes; gulong algo; g_assert (line); /* Eat space at the front */ skip_spaces (&line, &length); /* Blank line or comment */ if (length == 0 || line[0] == '#') return GCR_ERROR_UNRECOGNIZED; if (!next_word (&line, &length, &word_algo, &len_algo)) return GCR_ERROR_UNRECOGNIZED; /* * If the first word is not the algorithm, then we have options: * * option,option ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAI...EAz8Ji= Label here * * If the first word is the algorithm, then we have no options: * * ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAI...EAz8Ji= Label here */ algo = keytype_to_algo (word_algo, len_algo); if (algo == G_MAXULONG) { word_options = word_algo; len_options = len_algo; if (!next_word (&line, &length, &word_algo, &len_algo)) return GCR_ERROR_UNRECOGNIZED; algo = keytype_to_algo (word_algo, len_algo); if (algo == G_MAXULONG) return GCR_ERROR_UNRECOGNIZED; } else { word_options = NULL; len_options = 0; } /* Must have at least two words */ if (!next_word (&line, &length, &word_key, &len_key)) return GCR_ERROR_FAILURE; if (!decode_v2_public_key (algo, word_key, len_key, &builder)) { gck_builder_clear (&builder); return GCR_ERROR_FAILURE; } if (word_options) options = g_strndup (word_options, len_options); else options = NULL; /* The remainder of the line is the label */ skip_spaces (&line, &length); if (length > 0) { label = g_strndup (line, length); g_strstrip (label); gck_builder_add_string (&builder, CKA_LABEL, label); } attrs = gck_builder_end (&builder); if (callback != NULL) { bytes = g_bytes_new_with_free_func (outer, n_outer, (GDestroyNotify)g_bytes_unref, g_bytes_ref (backing)); (callback) (attrs, label, options, bytes, user_data); g_bytes_unref (bytes); } gck_attributes_unref (attrs); g_free (options); g_free (label); return GCR_SUCCESS; } guint _gcr_openssh_pub_parse (GBytes *data, GcrOpensshPubCallback callback, gpointer user_data) { const gchar *line; const gchar *end; gsize length; gboolean last; GcrDataError res; guint num_parsed; g_return_val_if_fail (data != NULL, FALSE); line = g_bytes_get_data (data, NULL); length = g_bytes_get_size (data); last = FALSE; num_parsed = 0; for (;;) { end = memchr (line, '\n', length); if (end == NULL) { end = line + length; last = TRUE; } if (line != end) { res = parse_v2_public_line (line, end - line, data, callback, user_data); if (res == GCR_ERROR_UNRECOGNIZED) res = parse_v1_public_line (line, end - line, data, callback, user_data); if (res == GCR_SUCCESS) num_parsed++; } if (last) break; end++; length -= (end - line); line = end; } return num_parsed; }