diff options
author | Dimitrij <kvarkas@gmail.com> | 2022-10-31 00:45:23 +0300 |
---|---|---|
committer | Dimitrij <kvarkas@gmail.com> | 2022-10-31 00:45:23 +0300 |
commit | 302fb2e8ddea1c993552c9a30c02f41d01ca54a9 (patch) | |
tree | d6cf1b32664296ef2cecda33caeafbe39e6695c1 /ssh/ca-config.c | |
parent | 59105d9b26363e47f00676bd365b2ac8d4cb536a (diff) | |
parent | 4ff82ab29a22936b78510c68f544a99e677efed3 (diff) |
Diffstat (limited to 'ssh/ca-config.c')
-rw-r--r-- | ssh/ca-config.c | 497 |
1 files changed, 497 insertions, 0 deletions
diff --git a/ssh/ca-config.c b/ssh/ca-config.c new file mode 100644 index 00000000..8c180b36 --- /dev/null +++ b/ssh/ca-config.c @@ -0,0 +1,497 @@ +/* + * Define and handle the configuration dialog box for SSH host CAs, + * using the same portable dialog specification API as config.c. + */ + +#include "putty.h" +#include "dialog.h" +#include "storage.h" +#include "tree234.h" +#include "ssh.h" + +const bool has_ca_config_box = true; + +#define NRSATYPES 3 + +struct ca_state { + dlgcontrol *ca_name_edit; + dlgcontrol *ca_reclist; + dlgcontrol *ca_pubkey_edit; + dlgcontrol *ca_pubkey_info; + dlgcontrol *ca_validity_edit; + dlgcontrol *rsa_type_checkboxes[NRSATYPES]; + char *name, *pubkey, *validity; + tree234 *ca_names; /* stores plain 'char *' */ + ca_options opts; + strbuf *ca_pubkey_blob; +}; + +static int ca_name_compare(void *av, void *bv) +{ + return strcmp((const char *)av, (const char *)bv); +} + +static inline void clear_string_tree(tree234 *t) +{ + char *p; + while ((p = delpos234(t, 0)) != NULL) + sfree(p); +} + +static void ca_state_free(void *vctx) +{ + struct ca_state *st = (struct ca_state *)vctx; + clear_string_tree(st->ca_names); + freetree234(st->ca_names); + sfree(st->name); + sfree(st->validity); + sfree(st); +} + +static void ca_refresh_name_list(struct ca_state *st) +{ + clear_string_tree(st->ca_names); + + host_ca_enum *hce = enum_host_ca_start(); + if (hce) { + strbuf *namebuf = strbuf_new(); + + while (strbuf_clear(namebuf), enum_host_ca_next(hce, namebuf)) { + char *name = dupstr(namebuf->s); + char *added = add234(st->ca_names, name); + /* Just imaginable that concurrent filesystem access might + * cause a repetition; avoid leaking memory if so */ + if (added != name) + sfree(name); + } + + strbuf_free(namebuf); + enum_host_ca_finish(hce); + } +} + +static void set_from_hca(struct ca_state *st, host_ca *hca) +{ + sfree(st->name); + st->name = dupstr(hca->name ? hca->name : ""); + + sfree(st->pubkey); + if (hca->ca_public_key) + st->pubkey = strbuf_to_str( + base64_encode_sb(ptrlen_from_strbuf(hca->ca_public_key), 0)); + else + st->pubkey = dupstr(""); + + st->validity = dupstr(hca->validity_expression ? + hca->validity_expression : ""); + + st->opts = hca->opts; /* structure copy */ +} + +static void ca_refresh_pubkey_info(struct ca_state *st, dlgparam *dp) +{ + char *text = NULL; + ssh_key *key = NULL; + strbuf *blob = strbuf_new(); + + ptrlen data = ptrlen_from_asciz(st->pubkey); + + if (st->ca_pubkey_blob) + strbuf_free(st->ca_pubkey_blob); + st->ca_pubkey_blob = NULL; + + if (!data.len) { + text = dupstr(" "); + goto out; + } + + /* + * See if we have a plain base64-encoded public key blob. + */ + if (base64_valid(data)) { + base64_decode_bs(BinarySink_UPCAST(blob), data); + } else { + /* + * Otherwise, try to decode as if it was a public key _file_. + */ + BinarySource src[1]; + BinarySource_BARE_INIT_PL(src, data); + const char *error; + if (!ppk_loadpub_s(src, NULL, BinarySink_UPCAST(blob), NULL, &error)) { + text = dupprintf("Cannot decode key: %s", error); + goto out; + } + } + + ptrlen alg_name = pubkey_blob_to_alg_name(ptrlen_from_strbuf(blob)); + if (!alg_name.len) { + text = dupstr("Invalid key (no key type)"); + goto out; + } + + const ssh_keyalg *alg = find_pubkey_alg_len(alg_name); + if (!alg) { + text = dupprintf("Unrecognised key type '%.*s'", + PTRLEN_PRINTF(alg_name)); + goto out; + } + if (alg->is_certificate) { + text = dupprintf("CA key may not be a certificate (type is '%.*s')", + PTRLEN_PRINTF(alg_name)); + goto out; + } + + key = ssh_key_new_pub(alg, ptrlen_from_strbuf(blob)); + if (!key) { + text = dupprintf("Invalid '%.*s' key data", PTRLEN_PRINTF(alg_name)); + goto out; + } + + text = ssh2_fingerprint(key, SSH_FPTYPE_DEFAULT); + st->ca_pubkey_blob = blob; + blob = NULL; /* prevent free */ + + out: + dlg_text_set(st->ca_pubkey_info, dp, text); + if (key) + ssh_key_free(key); + sfree(text); + if (blob) + strbuf_free(blob); +} + +static void ca_load_selected_record(struct ca_state *st, dlgparam *dp) +{ + int i = dlg_listbox_index(st->ca_reclist, dp); + if (i < 0) { + dlg_beep(dp); + return; + } + const char *name = index234(st->ca_names, i); + if (!name) { /* in case the list box and the tree got out of sync */ + dlg_beep(dp); + return; + } + host_ca *hca = host_ca_load(name); + if (!hca) { + char *msg = dupprintf("Unable to load host CA record '%s'", name); + dlg_error_msg(dp, msg); + sfree(msg); + return; + } + + set_from_hca(st, hca); + host_ca_free(hca); + + dlg_refresh(st->ca_name_edit, dp); + dlg_refresh(st->ca_pubkey_edit, dp); + dlg_refresh(st->ca_validity_edit, dp); + for (size_t i = 0; i < NRSATYPES; i++) + dlg_refresh(st->rsa_type_checkboxes[i], dp); + ca_refresh_pubkey_info(st, dp); +} + +static void ca_ok_handler(dlgcontrol *ctrl, dlgparam *dp, + void *data, int event) +{ + if (event == EVENT_ACTION) + dlg_end(dp, 0); +} + +static void ca_name_handler(dlgcontrol *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->context.p; + if (event == EVENT_REFRESH) { + dlg_editbox_set(ctrl, dp, st->name); + } else if (event == EVENT_VALCHANGE) { + sfree(st->name); + st->name = dlg_editbox_get(ctrl, dp); + + /* + * Try to auto-select the typed name in the list. + */ + int index; + if (!findrelpos234(st->ca_names, st->name, NULL, REL234_GE, &index)) + index = count234(st->ca_names) - 1; + if (index >= 0) + dlg_listbox_select(st->ca_reclist, dp, index); + } +} + +static void ca_reclist_handler(dlgcontrol *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->context.p; + if (event == EVENT_REFRESH) { + dlg_update_start(ctrl, dp); + dlg_listbox_clear(ctrl, dp); + const char *name; + for (int i = 0; (name = index234(st->ca_names, i)) != NULL; i++) + dlg_listbox_add(ctrl, dp, name); + dlg_update_done(ctrl, dp); + } else if (event == EVENT_ACTION) { + /* Double-clicking a session loads it */ + ca_load_selected_record(st, dp); + } +} + +static void ca_load_handler(dlgcontrol *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->context.p; + if (event == EVENT_ACTION) { + ca_load_selected_record(st, dp); + } +} + +static void ca_save_handler(dlgcontrol *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->context.p; + if (event == EVENT_ACTION) { + if (!*st->validity) { + dlg_error_msg(dp, "No validity expression configured " + "for this key"); + return; + } + + char *error_msg; + ptrlen error_loc; + if (!cert_expr_valid(st->validity, &error_msg, &error_loc)) { + char *error_full = dupprintf("Error in expression: %s", error_msg); + dlg_error_msg(dp, error_full); + dlg_set_focus(st->ca_validity_edit, dp); + dlg_editbox_select_range( + st->ca_validity_edit, dp, + (const char *)error_loc.ptr - st->validity, error_loc.len); + sfree(error_msg); + sfree(error_full); + return; + } + + if (!st->ca_pubkey_blob) { + dlg_error_msg(dp, "No valid CA public key entered"); + return; + } + + host_ca *hca = snew(host_ca); + memset(hca, 0, sizeof(*hca)); + hca->name = dupstr(st->name); + hca->ca_public_key = strbuf_dup(ptrlen_from_strbuf( + st->ca_pubkey_blob)); + hca->validity_expression = dupstr(st->validity); + hca->opts = st->opts; /* structure copy */ + + char *error = host_ca_save(hca); + host_ca_free(hca); + + if (error) { + dlg_error_msg(dp, error); + sfree(error); + } else { + ca_refresh_name_list(st); + dlg_refresh(st->ca_reclist, dp); + } + } +} + +static void ca_delete_handler(dlgcontrol *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->context.p; + if (event == EVENT_ACTION) { + int i = dlg_listbox_index(st->ca_reclist, dp); + if (i < 0) { + dlg_beep(dp); + return; + } + const char *name = index234(st->ca_names, i); + if (!name) { /* in case the list box and the tree got out of sync */ + dlg_beep(dp); + return; + } + + char *error = host_ca_delete(name); + if (error) { + dlg_error_msg(dp, error); + sfree(error); + } else { + ca_refresh_name_list(st); + dlg_refresh(st->ca_reclist, dp); + } + } +} + +static void ca_pubkey_edit_handler(dlgcontrol *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->context.p; + if (event == EVENT_REFRESH) { + dlg_editbox_set(ctrl, dp, st->pubkey); + } else if (event == EVENT_VALCHANGE) { + sfree(st->pubkey); + st->pubkey = dlg_editbox_get(ctrl, dp); + ca_refresh_pubkey_info(st, dp); + } +} + +static void ca_pubkey_file_handler(dlgcontrol *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->context.p; + if (event == EVENT_ACTION) { + Filename *filename = dlg_filesel_get(ctrl, dp); + strbuf *keyblob = strbuf_new(); + const char *load_error; + bool ok = ppk_loadpub_f(filename, NULL, BinarySink_UPCAST(keyblob), + NULL, &load_error); + if (!ok) { + char *message = dupprintf( + "Unable to load public key from '%s': %s", + filename_to_str(filename), load_error); + dlg_error_msg(dp, message); + sfree(message); + } else { + sfree(st->pubkey); + st->pubkey = strbuf_to_str( + base64_encode_sb(ptrlen_from_strbuf(keyblob), 0)); + dlg_refresh(st->ca_pubkey_edit, dp); + } + filename_free(filename); + strbuf_free(keyblob); + } +} + +static void ca_validity_handler(dlgcontrol *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->context.p; + if (event == EVENT_REFRESH) { + dlg_editbox_set(ctrl, dp, st->validity); + } else if (event == EVENT_VALCHANGE) { + sfree(st->validity); + st->validity = dlg_editbox_get(ctrl, dp); + } +} + +static void ca_rsa_type_handler(dlgcontrol *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->context.p; + size_t offset = ctrl->context2.i; + bool *option = (bool *)((char *)&st->opts + offset); + + if (event == EVENT_REFRESH) { + dlg_checkbox_set(ctrl, dp, *option); + } else if (event == EVENT_VALCHANGE) { + *option = dlg_checkbox_get(ctrl, dp); + } +} + +void setup_ca_config_box(struct controlbox *b) +{ + struct controlset *s; + dlgcontrol *c; + + /* Internal state for manipulating the host CA system */ + struct ca_state *st = (struct ca_state *)ctrl_alloc_with_free( + b, sizeof(struct ca_state), ca_state_free); + memset(st, 0, sizeof(*st)); + st->ca_names = newtree234(ca_name_compare); + st->validity = dupstr(""); + ca_refresh_name_list(st); + + /* Initialise the settings to a default blank host_ca */ + { + host_ca *hca = host_ca_new(); + set_from_hca(st, hca); + host_ca_free(hca); + } + + /* Action area, with the Done button in it */ + s = ctrl_getset(b, "", "", ""); + ctrl_columns(s, 5, 20, 20, 20, 20, 20); + c = ctrl_pushbutton(s, "Done", 'o', HELPCTX(ssh_kex_cert), + ca_ok_handler, P(st)); + c->button.iscancel = true; + c->column = 4; + + /* Load/save box, as similar as possible to the main saved sessions one */ + s = ctrl_getset(b, "Main", "loadsave", + "Load, save or delete a host CA record"); + ctrl_columns(s, 2, 75, 25); + c = ctrl_editbox(s, "Name for this CA (shown in log messages)", + 'n', 100, HELPCTX(ssh_kex_cert), + ca_name_handler, P(st), P(NULL)); + c->column = 0; + st->ca_name_edit = c; + /* Reset columns so that the buttons are alongside the list, rather + * than alongside that edit box. */ + ctrl_columns(s, 1, 100); + ctrl_columns(s, 2, 75, 25); + c = ctrl_listbox(s, NULL, NO_SHORTCUT, HELPCTX(ssh_kex_cert), + ca_reclist_handler, P(st)); + c->column = 0; + c->listbox.height = 6; + st->ca_reclist = c; + c = ctrl_pushbutton(s, "Load", 'l', HELPCTX(ssh_kex_cert), + ca_load_handler, P(st)); + c->column = 1; + c = ctrl_pushbutton(s, "Save", 'v', HELPCTX(ssh_kex_cert), + ca_save_handler, P(st)); + c->column = 1; + c = ctrl_pushbutton(s, "Delete", 'd', HELPCTX(ssh_kex_cert), + ca_delete_handler, P(st)); + c->column = 1; + + s = ctrl_getset(b, "Main", "pubkey", "Public key for this CA record"); + + ctrl_columns(s, 2, 75, 25); + c = ctrl_editbox(s, "Public key of certification authority", 'k', 100, + HELPCTX(ssh_kex_cert), ca_pubkey_edit_handler, + P(st), P(NULL)); + c->column = 0; + st->ca_pubkey_edit = c; + c = ctrl_filesel(s, "Read from file", NO_SHORTCUT, NULL, false, + "Select public key file of certification authority", + HELPCTX(ssh_kex_cert), ca_pubkey_file_handler, P(st)); + c->fileselect.just_button = true; + c->align_next_to = st->ca_pubkey_edit; + c->column = 1; + ctrl_columns(s, 1, 100); + st->ca_pubkey_info = c = ctrl_text(s, " ", HELPCTX(ssh_kex_cert)); + c->text.wrap = false; + + s = ctrl_getset(b, "Main", "options", "What this CA is trusted to do"); + + c = ctrl_editbox(s, "Valid hosts this key is trusted to certify", 'h', 100, + HELPCTX(ssh_cert_valid_expr), ca_validity_handler, + P(st), P(NULL)); + st->ca_validity_edit = c; + + ctrl_columns(s, 4, 44, 18, 18, 18); + c = ctrl_text(s, "Signature types (RSA keys only):", + HELPCTX(ssh_cert_rsa_hash)); + c->column = 0; + dlgcontrol *sigtypelabel = c; + c = ctrl_checkbox(s, "SHA-1", NO_SHORTCUT, HELPCTX(ssh_cert_rsa_hash), + ca_rsa_type_handler, P(st)); + c->column = 1; + c->align_next_to = sigtypelabel; + c->context2 = I(offsetof(ca_options, permit_rsa_sha1)); + st->rsa_type_checkboxes[0] = c; + c = ctrl_checkbox(s, "SHA-256", NO_SHORTCUT, HELPCTX(ssh_cert_rsa_hash), + ca_rsa_type_handler, P(st)); + c->column = 2; + c->align_next_to = sigtypelabel; + c->context2 = I(offsetof(ca_options, permit_rsa_sha256)); + st->rsa_type_checkboxes[1] = c; + c = ctrl_checkbox(s, "SHA-512", NO_SHORTCUT, HELPCTX(ssh_cert_rsa_hash), + ca_rsa_type_handler, P(st)); + c->column = 3; + c->align_next_to = sigtypelabel; + c->context2 = I(offsetof(ca_options, permit_rsa_sha512)); + st->rsa_type_checkboxes[2] = c; + ctrl_columns(s, 1, 100); +} |