1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-24 16:52:24 +00:00

Initial support for host certificates.

Now we offer the OpenSSH certificate key types in our KEXINIT host key
algorithm list, so that if the server has a certificate, they can send
it to us.

There's a new storage.h abstraction for representing a list of trusted
host CAs, and which ones are trusted to certify hosts for what
domains. This is stored outside the normal saved session data, because
the whole point of host certificates is to avoid per-host faffing.

Configuring this set of trusted CAs is done via a new GUI dialog box,
separate from the main PuTTY config box (because it modifies a single
set of settings across all saved sessions), which you can launch by
clicking a button in the 'Host keys' pane. The GUI is pretty crude for
the moment, and very much at a 'just about usable' stage right now. It
will want some polishing.

If we have no CA configured that matches the hostname, we don't offer
to receive certified host keys in the first place. So for existing
users who haven't set any of this up yet, nothing will immediately
change.

Currently, if we do offer to receive certified host keys and the
server presents one signed by a CA we don't trust, PuTTY will bomb out
unconditionally with an error, instead of offering a confirmation box.
That's an unfinished part which I plan to fix before this goes into a
release.
This commit is contained in:
Simon Tatham 2022-04-22 12:07:24 +01:00
parent df3a21d97b
commit 21d4754b6a
15 changed files with 1024 additions and 53 deletions

379
config.c
View File

@ -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;
}

2
defs.h
View File

@ -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;

View File

@ -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,

View File

@ -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,13 +852,7 @@ 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);
FingerprintType fptype_default =
@ -865,6 +860,76 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
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,
s->hkey, ssh_key_cache_id(s->hkey), s->keystr, keydisp,
@ -872,7 +937,6 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
ssh2_free_all_fingerprints(fingerprints);
sfree(keydisp);
}
#ifdef FUZZING
s->spr = SPR_OK;
#endif
@ -882,6 +946,7 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
ssh_spr_close(s->ppl.ssh, s->spr, "host key verification");
return;
}
}
/*
* Save this host key, to check against the one presented in

View File

@ -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.
*/
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++) {
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)
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,
ssh2_hostkey_algs[j].alg->cache_id)) {
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

View File

@ -59,6 +59,15 @@ struct kexinit_algorithm_list {
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;

View File

@ -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.
*/

View File

@ -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);
}

View File

@ -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
*

View File

@ -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

14
utils/host_ca_free.c Normal file
View File

@ -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);
}

View File

@ -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);
}

View File

@ -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"

View File

@ -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

View File

@ -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.
*/