diff --git a/config.c b/config.c index 3ab92bb8..01d58abd 100644 --- a/config.c +++ b/config.c @@ -9,6 +9,7 @@ #include "putty.h" #include "dialog.h" #include "storage.h" +#include "tree234.h" #define PRINTER_DISABLED_STRING "None (printing disabled)" @@ -1762,6 +1763,13 @@ void proxy_type_handler(union control *ctrl, dlgparam *dlg, } } +static void host_ca_button_handler(union control *ctrl, dlgparam *dp, + void *data, int event) +{ + if (event == EVENT_ACTION) + show_ca_config_box(dp); +} + void setup_config_box(struct controlbox *b, bool midsession, int protocol, int protcfginfo) { @@ -2826,6 +2834,16 @@ void setup_config_box(struct controlbox *b, bool midsession, ctrl_columns(s, 1, 100); } + /* + * But there's no reason not to forbid access to the host CA + * configuration box, which is common across sessions in any + * case. + */ + s = ctrl_getset(b, "Connection/SSH/Host keys", "ca", + "Configure trusted certification authorities"); + c = ctrl_pushbutton(s, "Configure host CAs", NO_SHORTCUT, + HELPCTX(no_help), host_ca_button_handler, I(0)); + if (!midsession || !(protcfginfo == 1 || protcfginfo == -1)) { /* * The Connection/SSH/Cipher panel. @@ -3298,3 +3316,364 @@ void setup_config_box(struct controlbox *b, bool midsession, I(CONF_supdup_scroll)); } } + +struct ca_state { + union control *ca_name_edit; + union control *ca_reclist; + union control *ca_pubkey_edit; + union control *ca_wclist; + union control *ca_wc_edit; + char *name, *pubkey, *wc; + tree234 *ca_names; /* stores plain 'char *' */ + tree234 *host_wcs; /* stores plain 'char *' */ +}; + +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); + clear_string_tree(st->host_wcs); + freetree234(st->host_wcs); + sfree(st->name); + sfree(st->wc); + 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 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; + } + + sfree(st->name); + st->name = dupstr(hca->name); + + sfree(st->pubkey); + st->pubkey = strbuf_to_str( + base64_encode_sb(ptrlen_from_strbuf(hca->ca_public_key), 0)); + + clear_string_tree(st->host_wcs); + for (size_t i = 0; i < hca->n_hostname_wildcards; i++) { + char *name = dupstr(hca->hostname_wildcards[i]); + char *added = add234(st->host_wcs, name); + if (added != name) + sfree(name); /* de-duplicate, just in case */ + } + + host_ca_free(hca); + + dlg_refresh(st->ca_name_edit, dp); + dlg_refresh(st->ca_pubkey_edit, dp); + dlg_refresh(st->ca_wclist, dp); +} + +static void ca_ok_handler(union control *ctrl, dlgparam *dp, + void *data, int event) +{ + if (event == EVENT_ACTION) + dlg_end(dp, 0); +} + +static void ca_name_handler(union control *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->generic.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(union control *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->generic.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(union control *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->generic.context.p; + if (event == EVENT_ACTION) { + ca_load_selected_record(st, dp); + } +} + +static void ca_save_handler(union control *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->generic.context.p; + if (event == EVENT_ACTION) { + host_ca *hca = snew(host_ca); + memset(hca, 0, sizeof(*hca)); + hca->name = dupstr(st->name); + hca->ca_public_key = base64_decode_sb(ptrlen_from_asciz(st->pubkey)); + hca->n_hostname_wildcards = count234(st->host_wcs); + hca->hostname_wildcards = snewn(hca->n_hostname_wildcards, char *); + for (size_t i = 0; i < hca->n_hostname_wildcards; i++) + hca->hostname_wildcards[i] = dupstr(index234(st->host_wcs, i)); + 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(union control *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->generic.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_handler(union control *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->generic.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); + } +} + +static void ca_wclist_handler(union control *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->generic.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->host_wcs, i)) != NULL; i++) + dlg_listbox_add(ctrl, dp, name); + dlg_update_done(ctrl, dp); + } +} + +static void ca_wc_edit_handler(union control *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->generic.context.p; + if (event == EVENT_REFRESH) { + dlg_editbox_set(ctrl, dp, st->wc); + } else if (event == EVENT_VALCHANGE) { + sfree(st->wc); + st->wc = dlg_editbox_get(ctrl, dp); + } +} + +static void ca_wc_add_handler(union control *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->generic.context.p; + if (event == EVENT_ACTION) { + if (!st->wc) { + dlg_beep(dp); + return; + } + + if (add234(st->host_wcs, st->wc) == st->wc) { + dlg_refresh(st->ca_wclist, dp); + } else { + sfree(st->wc); + } + + st->wc = dupstr(""); + dlg_refresh(st->ca_wc_edit, dp); + } +} + +static void ca_wc_rem_handler(union control *ctrl, dlgparam *dp, + void *data, int event) +{ + struct ca_state *st = (struct ca_state *)ctrl->generic.context.p; + if (event == EVENT_ACTION) { + int i = dlg_listbox_index(st->ca_wclist, dp); + if (i < 0) { + dlg_beep(dp); + return; + } + char *wc = delpos234(st->host_wcs, i); + if (!wc) { + dlg_beep(dp); + return; + } + + sfree(st->wc); + st->wc = wc; + dlg_refresh(st->ca_wclist, dp); + dlg_refresh(st->ca_wc_edit, dp); + } +} + +void setup_ca_config_box(struct controlbox *b) +{ + struct controlset *s; + union control *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->name = dupstr(""); + st->pubkey = dupstr(""); + st->ca_names = newtree234(ca_name_compare); + st->host_wcs = newtree234(ca_name_compare); + ca_refresh_name_list(st); + + /* 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(no_help), + ca_ok_handler, P(st)); + c->button.isdefault = true; + c->generic.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(no_help), + ca_name_handler, P(st), P(NULL)); + c->generic.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(no_help), + ca_reclist_handler, P(st)); + c->generic.column = 0; + c->listbox.height = 6; + st->ca_reclist = c; + c = ctrl_pushbutton(s, "Load", 'l', HELPCTX(no_help), + ca_load_handler, P(st)); + c->generic.column = 1; + c = ctrl_pushbutton(s, "Save", 'v', HELPCTX(no_help), + ca_save_handler, P(st)); + c->generic.column = 1; + c = ctrl_pushbutton(s, "Delete", 'd', HELPCTX(no_help), + ca_delete_handler, P(st)); + c->generic.column = 1; + + /* Box containing the details of a specific CA record */ + s = ctrl_getset(b, "Main", "details", "Details of a host CA record"); + c = ctrl_editbox(s, "Public key of certification authority", 'k', 100, + HELPCTX(no_help), ca_pubkey_handler, P(st), P(NULL)); + st->ca_pubkey_edit = c; + c = ctrl_listbox(s, "Hostname patterns this key is trusted to certify", + NO_SHORTCUT, HELPCTX(no_help), ca_wclist_handler, P(st)); + c->listbox.height = 3; + st->ca_wclist = c; + ctrl_columns(s, 3, 70, 15, 15); + c = ctrl_editbox(s, "Hostname pattern to add", 'h', 100, + HELPCTX(no_help), ca_wc_edit_handler, P(st), P(NULL)); + c->generic.column = 0; + st->ca_wc_edit = c; + c = ctrl_pushbutton(s, "Add", NO_SHORTCUT, HELPCTX(no_help), + ca_wc_add_handler, P(st)); + c->generic.column = 1; + c = ctrl_pushbutton(s, "Remove", NO_SHORTCUT, HELPCTX(no_help), + ca_wc_rem_handler, P(st)); + c->generic.column = 2; +} diff --git a/defs.h b/defs.h index 37cc7979..d9f2e355 100644 --- a/defs.h +++ b/defs.h @@ -176,6 +176,8 @@ typedef struct dlgparam dlgparam; typedef struct settings_w settings_w; typedef struct settings_r settings_r; typedef struct settings_e settings_e; +typedef struct host_ca host_ca; +typedef struct host_ca_enum host_ca_enum; typedef struct SessionSpecial SessionSpecial; diff --git a/putty.h b/putty.h index 44c82a41..5ceedbe1 100644 --- a/putty.h +++ b/putty.h @@ -2586,6 +2586,11 @@ void conf_fontsel_handler(union control *ctrl, dlgparam *dlg, void setup_config_box(struct controlbox *b, bool midsession, int protocol, int protcfginfo); +void setup_ca_config_box(struct controlbox *b); + +/* Platforms provide this to be called from config.c */ +void show_ca_config_box(dlgparam *dlg); + /* Visible outside config.c so that platforms can use it to recognise * the proxy type control */ void proxy_type_handler(union control *ctrl, dlgparam *dlg, diff --git a/ssh/kex2-client.c b/ssh/kex2-client.c index ff78840a..c01bd0fe 100644 --- a/ssh/kex2-client.c +++ b/ssh/kex2-client.c @@ -718,7 +718,8 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted) } } - s->keystr = (s->hkey ? ssh_key_cache_str(s->hkey) : NULL); + s->keystr = (s->hkey && !ssh_key_alg(s->hkey)->is_certificate ? + ssh_key_cache_str(s->hkey) : NULL); #ifndef NO_GSSAPI if (s->gss_kex_used) { /* @@ -851,19 +852,83 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted) } } - /* - * Authenticate remote host: verify host key. (We've already - * checked the signature of the exchange hash.) - */ - { - ssh2_userkey uk = { .key = s->hkey, .comment = NULL }; - char *keydisp = ssh2_pubkey_openssh_str(&uk); - char **fingerprints = ssh2_all_fingerprints(s->hkey); + ssh2_userkey uk = { .key = s->hkey, .comment = NULL }; + char **fingerprints = ssh2_all_fingerprints(s->hkey); - FingerprintType fptype_default = - ssh2_pick_default_fingerprint(fingerprints); - ppl_logevent("Host key fingerprint is:"); - ppl_logevent("%s", fingerprints[fptype_default]); + FingerprintType fptype_default = + ssh2_pick_default_fingerprint(fingerprints); + ppl_logevent("Host key fingerprint is:"); + ppl_logevent("%s", fingerprints[fptype_default]); + + /* + * Authenticate remote host: verify host key, either by + * certification or by the local host key cache. + * + * (We've already checked the signature of the exchange + * hash.) + */ + if (ssh_key_alg(s->hkey)->is_certificate) { + ssh2_free_all_fingerprints(fingerprints); + + char *base_fp = ssh2_fingerprint(ssh_key_base_key(s->hkey), + fptype_default); + ppl_logevent("Host key is a certificate, whose base key has " + "fingerprint:"); + ppl_logevent("%s", base_fp); + sfree(base_fp); + + strbuf *id_string = strbuf_new(); + StripCtrlChars *id_string_scc = stripctrl_new( + BinarySink_UPCAST(id_string), false, L'\0'); + ssh_key_cert_id_string( + s->hkey, BinarySink_UPCAST(id_string_scc)); + stripctrl_free(id_string_scc); + ppl_logevent("Certificate ID string is \"%s\"", id_string->s); + strbuf_free(id_string); + + strbuf *ca_pub = strbuf_new(); + ssh_key_ca_public_blob(s->hkey, BinarySink_UPCAST(ca_pub)); + host_ca hca_search = { .ca_public_key = ca_pub }; + host_ca *hca_found = find234(s->host_cas, &hca_search, NULL); + + char *ca_fp = ssh2_fingerprint_blob(ptrlen_from_strbuf(ca_pub), + fptype_default); + ppl_logevent("Fingerprint of certification authority:"); + ppl_logevent("%s", ca_fp); + sfree(ca_fp); + + strbuf_free(ca_pub); + + strbuf *error = strbuf_new(); + bool cert_ok = false; + + if (!hca_found) { + put_fmt(error, "Certification authority is not trusted"); + } else { + ppl_logevent("Certification authority matches '%s'", + hca_found->name); + cert_ok = ssh_key_check_cert( + s->hkey, + true, /* host certificate */ + ptrlen_from_asciz(s->savedhost), + time(NULL), + BinarySink_UPCAST(error)); + } + if (cert_ok) { + strbuf_free(error); + ppl_logevent("Accepted certificate"); + } else { + ppl_logevent("Rejected host key certificate: %s", + error->s); + ssh_sw_abort(s->ppl.ssh, + "Rejected host key certificate: %s", + error->s); + *aborted = true; + strbuf_free(error); + return; + } + } else { + char *keydisp = ssh2_pubkey_openssh_str(&uk); s->spr = verify_ssh_host_key( ppl_get_iseat(&s->ppl), s->conf, s->savedhost, s->savedport, @@ -872,15 +937,15 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted) ssh2_free_all_fingerprints(fingerprints); sfree(keydisp); - } #ifdef FUZZING - s->spr = SPR_OK; + s->spr = SPR_OK; #endif - crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE); - if (spr_is_abort(s->spr)) { - *aborted = true; - ssh_spr_close(s->ppl.ssh, s->spr, "host key verification"); - return; + crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE); + if (spr_is_abort(s->spr)) { + *aborted = true; + ssh_spr_close(s->ppl.ssh, s->spr, "host key verification"); + return; + } } /* diff --git a/ssh/transport2.c b/ssh/transport2.c index d1231069..ebb76be1 100644 --- a/ssh/transport2.c +++ b/ssh/transport2.c @@ -113,6 +113,7 @@ static const char *const kexlist_descr[NKEXLIST] = { }; static int weak_algorithm_compare(void *av, void *bv); +static int ca_blob_compare(void *av, void *bv); PacketProtocolLayer *ssh2_transport_new( Conf *conf, const char *host, int port, const char *fullhostname, @@ -134,6 +135,7 @@ PacketProtocolLayer *ssh2_transport_new( s->server_greeting = dupstr(server_greeting); s->stats = stats; s->hostkeyblob = strbuf_new(); + s->host_cas = newtree234(ca_blob_compare); pq_in_init(&s->pq_in_higher); pq_out_init(&s->pq_out_higher); @@ -212,6 +214,12 @@ static void ssh2_transport_free(PacketProtocolLayer *ppl) sfree(s->keystr); sfree(s->hostkey_str); strbuf_free(s->hostkeyblob); + { + host_ca *hca; + while ( (hca = delpos234(s->host_cas, 0)) ) + host_ca_free(hca); + freetree234(s->host_cas); + } if (s->hkey && !s->hostkeys) { ssh_key_free(s->hkey); s->hkey = NULL; @@ -493,7 +501,7 @@ static void ssh2_write_kexinit_lists( struct kexinit_algorithm_list kexlists[NKEXLIST], Conf *conf, const SshServerConfig *ssc, int remote_bugs, const char *hk_host, int hk_port, const ssh_keyalg *hk_prev, - ssh_transient_hostkey_cache *thc, + ssh_transient_hostkey_cache *thc, tree234 *host_cas, ssh_key *const *our_hostkeys, int our_nhostkeys, bool first_time, bool can_gssapi_keyex, bool transient_hostkey_mode) { @@ -672,33 +680,98 @@ static void ssh2_write_kexinit_lists( * they surely _do_ want to be alerted that a server * they're actually connecting to is using it. */ - warn = false; - for (i = 0; i < n_preferred_hk; i++) { - if (preferred_hk[i] == HK_WARN) - warn = true; - for (j = 0; j < lenof(ssh2_hostkey_algs); j++) { - if (ssh2_hostkey_algs[j].id != preferred_hk[i]) - continue; - if (conf_get_bool(conf, CONF_ssh_prefer_known_hostkeys) && - have_ssh_host_key(hk_host, hk_port, - ssh2_hostkey_algs[j].alg->cache_id)) { + + bool accept_certs = false; + { + host_ca_enum *handle = enum_host_ca_start(); + if (handle) { + strbuf *name = strbuf_new(); + while (strbuf_clear(name), enum_host_ca_next(handle, name)) { + host_ca *hca = host_ca_load(name->s); + if (!hca) + continue; + + bool match = false; + for (size_t i = 0, e = hca->n_hostname_wildcards; + i < e; i++) { + if (wc_match(hca->hostname_wildcards[i], hk_host)) { + match = true; + break; + } + } + + if (match && hca->ca_public_key) { + accept_certs = true; + add234(host_cas, hca); + } else { + host_ca_free(hca); + } + } + enum_host_ca_finish(handle); + strbuf_free(name); + } + } + + if (accept_certs) { + /* Add all the certificate algorithms first, in preference order */ + warn = false; + for (i = 0; i < n_preferred_hk; i++) { + if (preferred_hk[i] == HK_WARN) + warn = true; + for (j = 0; j < lenof(ssh2_hostkey_algs); j++) { + const struct ssh_signkey_with_user_pref_id *a = + &ssh2_hostkey_algs[j]; + if (!a->alg->is_certificate) + continue; + if (a->id != preferred_hk[i]) + continue; alg = ssh2_kexinit_addalg(&kexlists[KEXLIST_HOSTKEY], - ssh2_hostkey_algs[j].alg->ssh_id); - alg->u.hk.hostkey = ssh2_hostkey_algs[j].alg; + a->alg->ssh_id); + alg->u.hk.hostkey = a->alg; alg->u.hk.warn = warn; } } } + + /* Next, add uncertified algorithms we already know a key for + * (unless configured not to do that) */ warn = false; for (i = 0; i < n_preferred_hk; i++) { if (preferred_hk[i] == HK_WARN) warn = true; for (j = 0; j < lenof(ssh2_hostkey_algs); j++) { - if (ssh2_hostkey_algs[j].id != preferred_hk[i]) + const struct ssh_signkey_with_user_pref_id *a = + &ssh2_hostkey_algs[j]; + if (a->alg->is_certificate || !a->alg->cache_id) + continue; + if (a->id != preferred_hk[i]) + continue; + if (conf_get_bool(conf, CONF_ssh_prefer_known_hostkeys) && + have_ssh_host_key(hk_host, hk_port, + a->alg->cache_id)) { + alg = ssh2_kexinit_addalg(&kexlists[KEXLIST_HOSTKEY], + a->alg->ssh_id); + alg->u.hk.hostkey = a->alg; + alg->u.hk.warn = warn; + } + } + } + + /* And finally, everything else */ + warn = false; + for (i = 0; i < n_preferred_hk; i++) { + if (preferred_hk[i] == HK_WARN) + warn = true; + for (j = 0; j < lenof(ssh2_hostkey_algs); j++) { + const struct ssh_signkey_with_user_pref_id *a = + &ssh2_hostkey_algs[j]; + if (a->alg->is_certificate) + continue; + if (a->id != preferred_hk[i]) continue; alg = ssh2_kexinit_addalg(&kexlists[KEXLIST_HOSTKEY], - ssh2_hostkey_algs[j].alg->ssh_id); - alg->u.hk.hostkey = ssh2_hostkey_algs[j].alg; + a->alg->ssh_id); + alg->u.hk.hostkey = a->alg; alg->u.hk.warn = warn; } } @@ -1201,7 +1274,7 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl) ssh2_write_kexinit_lists( BinarySink_UPCAST(s->outgoing_kexinit), s->kexlists, s->conf, s->ssc, s->ppl.remote_bugs, - s->savedhost, s->savedport, s->hostkey_alg, s->thc, + s->savedhost, s->savedport, s->hostkey_alg, s->thc, s->host_cas, s->hostkeys, s->nhostkeys, !s->got_session_id, s->can_gssapi_keyex, s->gss_kex_used && !s->need_gss_transient_hostkey); @@ -1271,6 +1344,7 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl) for (i = 0; i < nhk; i++) { j = hks[i]; if (ssh2_hostkey_algs[j].alg != s->hostkey_alg && + ssh2_hostkey_algs[j].alg->cache_id && !have_ssh_host_key(s->savedhost, s->savedport, ssh2_hostkey_algs[j].alg->cache_id)) { s->uncert_hostkeys[s->n_uncert_hostkeys++] = j; @@ -2154,6 +2228,18 @@ static int weak_algorithm_compare(void *av, void *bv) return a < b ? -1 : a > b ? +1 : 0; } +static int ca_blob_compare(void *av, void *bv) +{ + host_ca *a = (host_ca *)av, *b = (host_ca *)bv; + strbuf *apk = a->ca_public_key, *bpk = b->ca_public_key; + /* Ordering by public key is arbitrary here, so do whatever is easiest */ + if (apk->len < bpk->len) + return -1; + if (apk->len > bpk->len) + return +1; + return memcmp(apk->u, bpk->u, apk->len); +} + /* * Wrapper on seat_confirm_weak_crypto_primitive(), which uses the * tree234 s->weak_algorithms_consented_to to ensure we ask at most diff --git a/ssh/transport2.h b/ssh/transport2.h index dc62f71f..72cd5fba 100644 --- a/ssh/transport2.h +++ b/ssh/transport2.h @@ -49,16 +49,25 @@ struct kexinit_algorithm_list { size_t nalgs, algsize; }; -#define HOSTKEY_ALGORITHMS(X) \ - X(HK_ED25519, ssh_ecdsa_ed25519) \ - X(HK_ED448, ssh_ecdsa_ed448) \ - X(HK_ECDSA, ssh_ecdsa_nistp256) \ - X(HK_ECDSA, ssh_ecdsa_nistp384) \ - X(HK_ECDSA, ssh_ecdsa_nistp521) \ - X(HK_DSA, ssh_dsa) \ - X(HK_RSA, ssh_rsa_sha512) \ - X(HK_RSA, ssh_rsa_sha256) \ - X(HK_RSA, ssh_rsa) \ +#define HOSTKEY_ALGORITHMS(X) \ + X(HK_ED25519, ssh_ecdsa_ed25519) \ + X(HK_ED448, ssh_ecdsa_ed448) \ + X(HK_ECDSA, ssh_ecdsa_nistp256) \ + X(HK_ECDSA, ssh_ecdsa_nistp384) \ + X(HK_ECDSA, ssh_ecdsa_nistp521) \ + X(HK_DSA, ssh_dsa) \ + X(HK_RSA, ssh_rsa_sha512) \ + X(HK_RSA, ssh_rsa_sha256) \ + X(HK_RSA, ssh_rsa) \ + X(HK_ED25519, opensshcert_ssh_ecdsa_ed25519) \ + /* OpenSSH defines no certified version of Ed448 */ \ + X(HK_ECDSA, opensshcert_ssh_ecdsa_nistp256) \ + X(HK_ECDSA, opensshcert_ssh_ecdsa_nistp384) \ + X(HK_ECDSA, opensshcert_ssh_ecdsa_nistp521) \ + X(HK_DSA, opensshcert_ssh_dsa) \ + X(HK_RSA, opensshcert_ssh_rsa_sha512) \ + X(HK_RSA, opensshcert_ssh_rsa_sha256) \ + X(HK_RSA, opensshcert_ssh_rsa) \ /* end of list */ #define COUNT_HOSTKEY_ALGORITHM(type, alg) +1 #define N_HOSTKEY_ALGORITHMS (0 HOSTKEY_ALGORITHMS(COUNT_HOSTKEY_ALGORITHM)) @@ -168,6 +177,8 @@ struct ssh2_transport_state { bool gss_kex_used; + tree234 *host_cas; + int nbits, pbits; bool warn_kex, warn_hk, warn_cscipher, warn_sccipher; mp_int *p, *g, *e, *f; diff --git a/storage.h b/storage.h index 3e03181a..df0c3e29 100644 --- a/storage.h +++ b/storage.h @@ -91,6 +91,29 @@ int check_stored_host_key(const char *hostname, int port, void store_host_key(const char *hostname, int port, const char *keytype, const char *key); +/* ---------------------------------------------------------------------- + * Functions to access PuTTY's configuration for trusted host + * certification authorities. This must be stored separately from the + * saved-session data, because the whole point is to avoid having to + * configure CAs separately per session. + */ + +struct host_ca { + char *name; + strbuf *ca_public_key; + char **hostname_wildcards; + size_t n_hostname_wildcards; +}; + +host_ca_enum *enum_host_ca_start(void); +bool enum_host_ca_next(host_ca_enum *handle, strbuf *out); +void enum_host_ca_finish(host_ca_enum *handle); + +host_ca *host_ca_load(const char *name); +char *host_ca_save(host_ca *); /* NULL on success, or dynamic error msg */ +char *host_ca_delete(const char *name); /* likewise */ +void host_ca_free(host_ca *); + /* ---------------------------------------------------------------------- * Functions to access PuTTY's random number seed file. */ diff --git a/unix/dialog.c b/unix/dialog.c index 7d1514fd..edd08314 100644 --- a/unix/dialog.c +++ b/unix/dialog.c @@ -4219,3 +4219,84 @@ int gtkdlg_askappend(Seat *seat, Filename *filename, return -1; /* dialog still in progress */ } + +struct ca_config_box { + GtkWidget *window; + struct controlbox *cb; + struct Shortcuts scs; + dlgparam dp; +}; +static struct ca_config_box *cacfg; /* one of these, cross-instance */ + +static void cacfg_destroy(GtkWidget *widget, gpointer data) +{ + cacfg->window = NULL; + dlg_cleanup(&cacfg->dp); + ctrl_free_box(cacfg->cb); + cacfg->cb = NULL; +} +void show_ca_config_box(dlgparam *dp) +{ + if (!cacfg) { + cacfg = snew(struct ca_config_box); + memset(cacfg, 0, sizeof(*cacfg)); + } + + if (cacfg->window) { + /* This dialog box is already displayed; re-focus it */ + gtk_widget_grab_focus(cacfg->window); + return; + } + + dlg_init(&cacfg->dp); + for (size_t i = 0; i < lenof(cacfg->scs.sc); i++) { + cacfg->scs.sc[i].action = SHORTCUT_EMPTY; + } + + cacfg->cb = ctrl_new_box(); + setup_ca_config_box(cacfg->cb); + + cacfg->window = our_dialog_new(); + gtk_window_set_title(GTK_WINDOW(cacfg->window), + "PuTTY trusted host certification authorities"); + gtk_widget_set_size_request( + cacfg->window, string_width( + "ecdsa-sha2-nistp256 256 SHA256:hsO5a8MYGzBoa2gW5" + "dLV2vl7bTnCPjw64x3kLkz6BY8"), -1); + + /* Set up everything else */ + for (int i = 0; i < cacfg->cb->nctrlsets; i++) { + struct controlset *s = cacfg->cb->ctrlsets[i]; + GtkWidget *w = layout_ctrls(&cacfg->dp, NULL, &cacfg->scs, s, + GTK_WINDOW(cacfg->window)); + gtk_container_set_border_width(GTK_CONTAINER(w), 10); + gtk_widget_show(w); + + if (!*s->pathname) { + our_dialog_set_action_area(GTK_WINDOW(cacfg->window), w); + } else { + our_dialog_add_to_content_area(GTK_WINDOW(cacfg->window), w, + true, true, 0); + } + } + + cacfg->dp.data = cacfg; + cacfg->dp.shortcuts = &cacfg->scs; + cacfg->dp.lastfocus = NULL; + cacfg->dp.retval = 0; + cacfg->dp.window = cacfg->window; + + dlg_refresh(NULL, &cacfg->dp); + + if (dp) { + set_transient_window_pos(dp->window, cacfg->window); + } else { + gtk_window_set_position(GTK_WINDOW(cacfg->window), GTK_WIN_POS_CENTER); + } + gtk_widget_show(cacfg->window); + + g_signal_connect(G_OBJECT(cacfg->window), "destroy", + G_CALLBACK(cacfg_destroy), NULL); + g_signal_connect(G_OBJECT(cacfg->window), "key_press_event", + G_CALLBACK(win_key_press), &cacfg->dp); +} diff --git a/unix/storage.c b/unix/storage.c index c61fb526..8be482cd 100644 --- a/unix/storage.c +++ b/unix/storage.c @@ -28,7 +28,7 @@ enum { INDEX_DIR, INDEX_HOSTKEYS, INDEX_HOSTKEYS_TMP, INDEX_RANDSEED, - INDEX_SESSIONDIR, INDEX_SESSION, + INDEX_SESSIONDIR, INDEX_SESSION, INDEX_HOSTCADIR, INDEX_HOSTCA }; static const char hex[16] = "0123456789ABCDEF"; @@ -202,6 +202,23 @@ static char *make_filename(int index, const char *subname) sfree(tmp); return ret; } + if (index == INDEX_HOSTCADIR) { + env = getenv("PUTTYSSHHOSTCAS"); + if (env) + return dupstr(env); + tmp = make_filename(INDEX_DIR, NULL); + ret = dupprintf("%s/sshhostcas", tmp); + sfree(tmp); + return ret; + } + if (index == INDEX_HOSTCA) { + strbuf *sb = strbuf_new(); + tmp = make_filename(INDEX_HOSTCADIR, NULL); + put_fmt(sb, "%s/", tmp); + sfree(tmp); + make_session_filename(subname, sb); + return strbuf_to_str(sb); + } tmp = make_filename(INDEX_DIR, NULL); ret = dupprintf("%s/ERROR", tmp); sfree(tmp); @@ -545,25 +562,25 @@ settings_e *enum_settings_start(void) return toret; } -bool enum_settings_next(settings_e *handle, strbuf *out) +static bool enum_dir_next(DIR *dp, int index, strbuf *out) { struct dirent *de; struct stat st; strbuf *fullpath; - if (!handle->dp) - return NULL; + if (!dp) + return false; fullpath = strbuf_new(); - char *sessiondir = make_filename(INDEX_SESSIONDIR, NULL); + char *sessiondir = make_filename(index, NULL); put_dataz(fullpath, sessiondir); sfree(sessiondir); put_byte(fullpath, '/'); size_t baselen = fullpath->len; - while ( (de = readdir(handle->dp)) != NULL ) { + while ( (de = readdir(dp)) != NULL ) { strbuf_shrink_to(fullpath, baselen); put_dataz(fullpath, de->d_name); @@ -579,6 +596,11 @@ bool enum_settings_next(settings_e *handle, strbuf *out) return false; } +bool enum_settings_next(settings_e *handle, strbuf *out) +{ + return enum_dir_next(handle->dp, INDEX_SESSIONDIR, out); +} + void enum_settings_finish(settings_e *handle) { if (handle->dp) @@ -586,6 +608,117 @@ void enum_settings_finish(settings_e *handle) sfree(handle); } +struct host_ca_enum { + DIR *dp; +}; + +host_ca_enum *enum_host_ca_start(void) +{ + host_ca_enum *handle = snew(host_ca_enum); + + char *filename = make_filename(INDEX_HOSTCADIR, NULL); + handle->dp = opendir(filename); + sfree(filename); + + return handle; +} + +bool enum_host_ca_next(host_ca_enum *handle, strbuf *out) +{ + return enum_dir_next(handle->dp, INDEX_HOSTCADIR, out); +} + +void enum_host_ca_finish(host_ca_enum *handle) +{ + if (handle->dp) + closedir(handle->dp); + sfree(handle); +} + +host_ca *host_ca_load(const char *name) +{ + char *filename = make_filename(INDEX_HOSTCA, name); + FILE *fp = fopen(filename, "r"); + sfree(filename); + if (!fp) + return NULL; + + host_ca *hca = snew(host_ca); + memset(hca, 0, sizeof(*hca)); + hca->name = dupstr(name); + + size_t wcsize = 0; + char *line; + + while ( (line = fgetline(fp)) ) { + char *value = strchr(line, '='); + + if (!value) { + sfree(line); + continue; + } + *value++ = '\0'; + value[strcspn(value, "\r\n")] = '\0'; /* trim trailing NL */ + + if (!strcmp(line, "PublicKey")) { + hca->ca_public_key = base64_decode_sb(ptrlen_from_asciz(value)); + } else if (!strcmp(line, "MatchHosts")) { + sgrowarray(hca->hostname_wildcards, wcsize, + hca->n_hostname_wildcards); + hca->hostname_wildcards[hca->n_hostname_wildcards++] = + dupstr(value); + } + + sfree(line); + } + + return hca; +} + +char *host_ca_save(host_ca *hca) +{ + if (!*hca->name) + return dupstr("CA record must have a name"); + + char *filename = make_filename(INDEX_HOSTCA, hca->name); + FILE *fp = fopen(filename, "w"); + if (!fp) + return dupprintf("Unable to open file '%s'", filename); + + fprintf(fp, "PublicKey="); + base64_encode_fp(fp, ptrlen_from_strbuf(hca->ca_public_key), 0); + fprintf(fp, "\n"); + + for (size_t i = 0; i < hca->n_hostname_wildcards; i++) + fprintf(fp, "MatchHosts=%s\n", hca->hostname_wildcards[i]); + + bool bad = ferror(fp); + if (fclose(fp) < 0) + bad = true; + + char *err = NULL; + if (bad) + err = dupprintf("Unable to write file '%s'", filename); + + sfree(filename); + return err; +} + +char *host_ca_delete(const char *name) +{ + if (!*name) + return dupstr("CA record must have a name"); + char *filename = make_filename(INDEX_HOSTCA, name); + bool bad = remove(filename) < 0; + + char *err = NULL; + if (bad) + err = dupprintf("Unable to delete file '%s'", filename); + + sfree(filename); + return err; +} + /* * Lines in the host keys file are of the form * diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 74ce4d54..949166f5 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -25,6 +25,7 @@ add_sources_from_current_dir(utils encode_utf8.c encode_wide_string_as_utf8.c fgetline.c + host_ca_free.c host_strchr.c host_strchr_internal.c host_strcspn.c diff --git a/utils/host_ca_free.c b/utils/host_ca_free.c new file mode 100644 index 00000000..5fb3a46c --- /dev/null +++ b/utils/host_ca_free.c @@ -0,0 +1,14 @@ +#include "defs.h" +#include "misc.h" +#include "storage.h" + +void host_ca_free(host_ca *hca) +{ + sfree(hca->name); + if (hca->ca_public_key) + strbuf_free(hca->ca_public_key); + for (size_t i = 0; i < hca->n_hostname_wildcards; i++) + sfree(hca->hostname_wildcards[i]); + sfree(hca->hostname_wildcards); + sfree(hca); +} diff --git a/windows/dialog.c b/windows/dialog.c index 7e0c92ad..774214db 100644 --- a/windows/dialog.c +++ b/windows/dialog.c @@ -1171,3 +1171,41 @@ void old_keyfile_warning(void) sfree(msg); sfree(title); } + +static INT_PTR CAConfigProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, + void *ctx) +{ + PortableDialogStuff *pds = (PortableDialogStuff *)ctx; + + switch (msg) { + case WM_INITDIALOG: + pds_initdialog_start(pds, hwnd); + + SendMessage(hwnd, WM_SETICON, (WPARAM) ICON_BIG, + (LPARAM) LoadIcon(hinst, MAKEINTRESOURCE(IDI_CFGICON))); + + centre_window(hwnd); + + pds_create_controls(pds, 0, IDCX_PANELBASE, 3, 3, 13, "Main"); + pds_create_controls(pds, 0, IDCX_STDBASE, 3, 3, 235, ""); + dlg_refresh(NULL, pds->dp); /* and set up control values */ + + pds_initdialog_finish(pds); + return 0; + + default: + return pds_default_dlgproc(pds, hwnd, msg, wParam, lParam); + } +} + +void show_ca_config_box(dlgparam *dp) +{ + PortableDialogStuff *pds = pds_new(1); + + setup_ca_config_box(pds->ctrlbox); + + ShinyDialogBox(hinst, MAKEINTRESOURCE(IDD_CA_CONFIG), "PuTTYConfigBox", + dp ? dp->hwnd : NULL, CAConfigProc, pds); + + pds_free(pds); +} diff --git a/windows/putty-common.rc2 b/windows/putty-common.rc2 index f8df971f..be5202aa 100644 --- a/windows/putty-common.rc2 +++ b/windows/putty-common.rc2 @@ -130,4 +130,13 @@ BEGIN DEFPUSHBUTTON "&Close", IDOK, 176, 130, 48, 14 END +/* Accelerators used: aco */ +IDD_CA_CONFIG DIALOG DISCARDABLE 0, 0, 300, 252 +STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "PuTTY trusted host certification authorities" +FONT 8, "MS Shell Dlg" +CLASS "PuTTYConfigBox" +BEGIN +END + #include "version.rc2" diff --git a/windows/putty-rc.h b/windows/putty-rc.h index 003609fd..c1658e5d 100644 --- a/windows/putty-rc.h +++ b/windows/putty-rc.h @@ -16,6 +16,7 @@ #define IDD_HK_ABSENT 114 #define IDD_HK_WRONG 115 #define IDD_HK_MOREINFO 116 +#define IDD_CA_CONFIG 117 #define IDN_LIST 1001 #define IDN_COPY 1002 diff --git a/windows/storage.c b/windows/storage.c index bf46cce0..33b77388 100644 --- a/windows/storage.c +++ b/windows/storage.c @@ -21,6 +21,7 @@ static const char *const reg_jumplist_key = PUTTY_REG_POS "\\Jumplist"; static const char *const reg_jumplist_value = "Recent sessions"; static const char *const puttystr = PUTTY_REG_POS "\\Sessions"; +static const char *const host_ca_key = PUTTY_REG_POS "\\SshHostCAs"; static bool tried_shgetfolderpath = false; static HMODULE shell32_module = NULL; @@ -371,6 +372,128 @@ void store_host_key(const char *hostname, int port, strbuf_free(regname); } +struct host_ca_enum { + HKEY key; + int i; +}; + +host_ca_enum *enum_host_ca_start(void) +{ + host_ca_enum *e; + HKEY key; + + if (!(key = open_regkey(false, HKEY_CURRENT_USER, host_ca_key))) + return NULL; + + e = snew(host_ca_enum); + e->key = key; + e->i = 0; + + return e; +} + +bool enum_host_ca_next(host_ca_enum *e, strbuf *sb) +{ + char *regbuf = enum_regkey(e->key, e->i); + if (!regbuf) + return false; + + unescape_registry_key(regbuf, sb); + sfree(regbuf); + e->i++; + return true; +} + +void enum_host_ca_finish(host_ca_enum *e) +{ + close_regkey(e->key); + sfree(e); +} + +host_ca *host_ca_load(const char *name) +{ + strbuf *sb; + const char *s; + + sb = strbuf_new(); + escape_registry_key(name, sb); + HKEY rkey = open_regkey(false, HKEY_CURRENT_USER, host_ca_key, sb->s); + strbuf_free(sb); + + if (!rkey) + return NULL; + + host_ca *hca = snew(host_ca); + memset(hca, 0, sizeof(*hca)); + hca->name = dupstr(name); + + if ((s = get_reg_sz(rkey, "PublicKey")) != NULL) + hca->ca_public_key = base64_decode_sb(ptrlen_from_asciz(s)); + + if ((sb = get_reg_multi_sz(rkey, "MatchHosts")) != NULL) { + BinarySource src[1]; + BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(sb)); + + const char *wc; + size_t wcsize = 0; + while (wc = get_asciz(src), !get_err(src)) { + sgrowarray(hca->hostname_wildcards, wcsize, + hca->n_hostname_wildcards); + hca->hostname_wildcards[hca->n_hostname_wildcards++] = dupstr(wc); + } + + strbuf_free(sb); + } + + close_regkey(rkey); + return hca; +} + +char *host_ca_save(host_ca *hca) +{ + if (!*hca->name) + return dupstr("CA record must have a name"); + + strbuf *sb = strbuf_new(); + escape_registry_key(hca->name, sb); + HKEY rkey = open_regkey(true, HKEY_CURRENT_USER, host_ca_key, sb->s); + if (!rkey) { + char *err = dupprintf("Unable to create registry key\n" + "HKEY_CURRENT_USER\\%s\\%s", host_ca_key, sb->s); + strbuf_free(sb); + return err; + } + strbuf_free(sb); + + strbuf *base64_pubkey = base64_encode_sb( + ptrlen_from_strbuf(hca->ca_public_key), 0); + put_reg_sz(rkey, "PublicKey", base64_pubkey->s); + strbuf_free(base64_pubkey); + + strbuf *wcs = strbuf_new(); + for (size_t i = 0; i < hca->n_hostname_wildcards; i++) + put_asciz(wcs, hca->hostname_wildcards[i]); + put_reg_multi_sz(rkey, "MatchHosts", wcs); + strbuf_free(wcs); + + close_regkey(rkey); + return NULL; +} + +char *host_ca_delete(const char *name) +{ + HKEY rkey = open_regkey(false, HKEY_CURRENT_USER, host_ca_key); + if (!rkey) + return NULL; + + strbuf *sb = strbuf_new(); + escape_registry_key(name, sb); + del_regkey(rkey, sb->s); + strbuf_free(sb); + + return NULL; +} + /* * Open (or delete) the random seed file. */