1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-07-02 03:52:49 -05:00

Merge tag '0.80'.

This involved a trivial merge conflict fix in terminal.c because of
the way the cherry-pick 73b41feba5 differed from its original
bdbd5f429c.

But a more significant rework was needed in windows/console.c, because
the updates to confirm_weak_* conflicted with the changes on main to
abstract out the ConsoleIO system.
This commit is contained in:
Simon Tatham
2023-12-18 14:32:57 +00:00
24 changed files with 803 additions and 283 deletions

View File

@ -138,12 +138,14 @@ void ssh2_bpp_new_outgoing_crypto(
BinaryPacketProtocol *bpp,
const ssh_cipheralg *cipher, const void *ckey, const void *iv,
const ssh2_macalg *mac, bool etm_mode, const void *mac_key,
const ssh_compression_alg *compression, bool delayed_compression);
const ssh_compression_alg *compression, bool delayed_compression,
bool reset_sequence_number);
void ssh2_bpp_new_incoming_crypto(
BinaryPacketProtocol *bpp,
const ssh_cipheralg *cipher, const void *ckey, const void *iv,
const ssh2_macalg *mac, bool etm_mode, const void *mac_key,
const ssh_compression_alg *compression, bool delayed_compression);
const ssh_compression_alg *compression, bool delayed_compression,
bool reset_sequence_number);
/*
* A query method specific to the interface between ssh2transport and

View File

@ -106,7 +106,8 @@ void ssh2_bpp_new_outgoing_crypto(
BinaryPacketProtocol *bpp,
const ssh_cipheralg *cipher, const void *ckey, const void *iv,
const ssh2_macalg *mac, bool etm_mode, const void *mac_key,
const ssh_compression_alg *compression, bool delayed_compression)
const ssh_compression_alg *compression, bool delayed_compression,
bool reset_sequence_number)
{
struct ssh2_bpp_state *s;
assert(bpp->vt == &ssh2_bpp_vtable);
@ -150,6 +151,9 @@ void ssh2_bpp_new_outgoing_crypto(
s->out.mac = NULL;
}
if (reset_sequence_number)
s->out.sequence = 0;
if (delayed_compression && !s->seen_userauth_success) {
s->out.pending_compression = compression;
s->out_comp = NULL;
@ -174,7 +178,8 @@ void ssh2_bpp_new_incoming_crypto(
BinaryPacketProtocol *bpp,
const ssh_cipheralg *cipher, const void *ckey, const void *iv,
const ssh2_macalg *mac, bool etm_mode, const void *mac_key,
const ssh_compression_alg *compression, bool delayed_compression)
const ssh_compression_alg *compression, bool delayed_compression,
bool reset_sequence_number)
{
struct ssh2_bpp_state *s;
assert(bpp->vt == &ssh2_bpp_vtable);
@ -231,6 +236,9 @@ void ssh2_bpp_new_incoming_crypto(
* start consuming the input data again. */
s->pending_newkeys = false;
if (reset_sequence_number)
s->in.sequence = 0;
/* And schedule a run of handle_input, in case there's already
* input data in the queue. */
queue_idempotent_callback(&s->bpp.ic_in_raw);

View File

@ -1085,6 +1085,110 @@ SeatPromptResult verify_ssh_host_key(
return toret;
}
SeatPromptResult confirm_weak_crypto_primitive(
InteractionReadySeat iseat, const char *algtype, const char *algname,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx,
WeakCryptoReason wcr)
{
SeatDialogText *text = seat_dialog_text_new();
const SeatDialogPromptDescriptions *pds =
seat_prompt_descriptions(iseat.seat);
seat_dialog_text_append(text, SDT_TITLE, "%s Security Alert", appname);
switch (wcr) {
case WCR_BELOW_THRESHOLD:
seat_dialog_text_append(
text, SDT_PARA,
"The first %s supported by the server is %s, "
"which is below the configured warning threshold.",
algtype, algname);
break;
case WCR_TERRAPIN:
case WCR_TERRAPIN_AVOIDABLE:
seat_dialog_text_append(
text, SDT_PARA,
"The %s selected for this session is %s, "
"which, with this server, is vulnerable to the 'Terrapin' attack "
"CVE-2023-48795, potentially allowing an attacker to modify "
"the encrypted session.",
algtype, algname);
seat_dialog_text_append(
text, SDT_PARA,
"Upgrading, patching, or reconfiguring this SSH server is the "
"best way to avoid this vulnerability, if possible.");
if (wcr == WCR_TERRAPIN_AVOIDABLE) {
seat_dialog_text_append(
text, SDT_PARA,
"You can also avoid this vulnerability by abandoning "
"this connection, moving ChaCha20 to below the "
"'warn below here' line in PuTTY's SSH cipher "
"configuration (so that an algorithm without the "
"vulnerability will be selected), and starting a new "
"connection.");
}
break;
default:
unreachable("bad WeakCryptoReason");
}
/* In batch mode, we print the above information and then this
* abort message, and stop. */
seat_dialog_text_append(text, SDT_BATCH_ABORT, "Connection abandoned.");
seat_dialog_text_append(
text, SDT_PARA, "To accept the risk and continue, %s. "
"To abandon the connection, %s.",
pds->weak_accept_action, pds->weak_cancel_action);
seat_dialog_text_append(text, SDT_PROMPT, "Continue with connection?");
SeatPromptResult toret = seat_confirm_weak_crypto_primitive(
iseat, text, callback, ctx);
seat_dialog_text_free(text);
return toret;
}
SeatPromptResult confirm_weak_cached_hostkey(
InteractionReadySeat iseat, const char *algname, const char **betteralgs,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
SeatDialogText *text = seat_dialog_text_new();
const SeatDialogPromptDescriptions *pds =
seat_prompt_descriptions(iseat.seat);
seat_dialog_text_append(text, SDT_TITLE, "%s Security Alert", appname);
seat_dialog_text_append(
text, SDT_PARA,
"The first host key type we have stored for this server "
"is %s, which is below the configured warning threshold.", algname);
seat_dialog_text_append(
text, SDT_PARA,
"The server also provides the following types of host key "
"above the threshold, which we do not have stored:");
for (const char **p = betteralgs; *p; p++)
seat_dialog_text_append(text, SDT_DISPLAY, "%s", *p);
/* In batch mode, we print the above information and then this
* abort message, and stop. */
seat_dialog_text_append(text, SDT_BATCH_ABORT, "Connection abandoned.");
seat_dialog_text_append(
text, SDT_PARA, "To accept the risk and continue, %s. "
"To abandon the connection, %s.",
pds->weak_accept_action, pds->weak_cancel_action);
seat_dialog_text_append(text, SDT_PROMPT, "Continue with connection?");
SeatPromptResult toret = seat_confirm_weak_cached_hostkey(
iseat, text, callback, ctx);
seat_dialog_text_free(text);
return toret;
}
/* ----------------------------------------------------------------------
* Common functions shared between SSH-1 layers.
*/

View File

@ -323,9 +323,9 @@ static void ssh1_login_process_queue(PacketProtocolLayer *ppl)
/* Warn about chosen cipher if necessary. */
if (warn) {
s->spr = seat_confirm_weak_crypto_primitive(
s->spr = confirm_weak_crypto_primitive(
ppl_get_iseat(&s->ppl), "cipher", cipher_string,
ssh1_login_dialog_callback, s);
ssh1_login_dialog_callback, s, WCR_BELOW_THRESHOLD);
crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
if (spr_is_abort(s->spr)) {
ssh_spr_close(s->ppl.ssh, s->spr, "cipher warning");

View File

@ -98,11 +98,11 @@ void mainchan_terminal_size(mainchan *mc, int width, int height) {}
/* Seat functions to ensure we don't get choosy about crypto - as the
* server, it's not up to us to give user warnings */
static SeatPromptResult server_confirm_weak_crypto_primitive(
Seat *seat, const char *algtype, const char *algname,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{ return SPR_OK; }
static SeatPromptResult server_confirm_weak_cached_hostkey(
Seat *seat, const char *algname, const char *betteralgs,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{ return SPR_OK; }

View File

@ -27,6 +27,18 @@ const static ssh2_macalg *const buggymacs[] = {
&ssh_hmac_sha1_buggy, &ssh_hmac_sha1_96_buggy, &ssh_hmac_md5
};
const static ptrlen ext_info_c = PTRLEN_DECL_LITERAL("ext-info-c");
const static ptrlen ext_info_s = PTRLEN_DECL_LITERAL("ext-info-s");
const static ptrlen kex_strict_c =
PTRLEN_DECL_LITERAL("kex-strict-c-v00@openssh.com");
const static ptrlen kex_strict_s =
PTRLEN_DECL_LITERAL("kex-strict-s-v00@openssh.com");
/* Pointer value to store in s->weak_algorithms_consented_to to
* indicate that the user has accepted the risk of the Terrapin
* attack */
static const char terrapin_weakness[1];
static ssh_compressor *ssh_comp_none_init(void)
{
return NULL;
@ -81,6 +93,9 @@ static void ssh2_transport_set_max_data_size(struct ssh2_transport_state *s);
static unsigned long sanitise_rekey_time(int rekey_time, unsigned long def);
static void ssh2_transport_higher_layer_packet_callback(void *context);
static void ssh2_transport_final_output(PacketProtocolLayer *ppl);
static const char *terrapin_vulnerable(
bool strict_kex, const transport_direction *d);
static bool try_to_avoid_terrapin(const struct ssh2_transport_state *s);
static const PacketProtocolLayerVtable ssh2_transport_vtable = {
.free = ssh2_transport_free,
@ -102,7 +117,7 @@ static bool ssh2_transport_timer_update(struct ssh2_transport_state *s,
unsigned long rekey_time);
static SeatPromptResult ssh2_transport_confirm_weak_crypto_primitive(
struct ssh2_transport_state *s, const char *type, const char *name,
const void *alg);
const void *alg, WeakCryptoReason wcr);
static const char *const kexlist_descr[NKEXLIST] = {
"key exchange algorithm",
@ -462,6 +477,31 @@ static bool ssh2_transport_filter_queue(struct ssh2_transport_state *s)
{
PktIn *pktin;
if (!s->enabled_incoming_crypto) {
/*
* Record the fact that we've seen any non-KEXINIT packet at
* the head of our queue.
*
* This enables us to check later that the initial incoming
* KEXINIT was the very first packet, if scanning the KEXINITs
* turns out to enable strict-kex mode.
*/
PktIn *pktin = pq_peek(s->ppl.in_pq);
if (pktin && pktin->type != SSH2_MSG_KEXINIT)
s->seen_non_kexinit = true;
if (s->strict_kex) {
/*
* Also, if we're already in strict-KEX mode and haven't
* turned on crypto yet, don't do any actual filtering.
* This ensures that extraneous packets _after_ the
* KEXINIT will go to the main coroutine, which will
* complain about them.
*/
return false;
}
}
while (1) {
if (ssh2_common_filter_queue(&s->ppl))
return true;
@ -937,10 +977,13 @@ static void ssh2_write_kexinit_lists(
add_to_commasep_pl(list, kexlists[i].algs[j].name);
}
if (i == KEXLIST_KEX && first_time) {
if (our_hostkeys) /* we're the server */
add_to_commasep(list, "ext-info-s");
else /* we're the client */
add_to_commasep(list, "ext-info-c");
if (our_hostkeys) { /* we're the server */
add_to_commasep_pl(list, ext_info_s);
add_to_commasep_pl(list, kex_strict_s);
} else { /* we're the client */
add_to_commasep_pl(list, ext_info_c);
add_to_commasep_pl(list, kex_strict_c);
}
}
put_stringsb(pktout, list);
}
@ -955,15 +998,37 @@ struct server_hostkeys {
size_t n, size;
};
static bool ssh2_scan_kexinits(
ptrlen client_kexinit, ptrlen server_kexinit,
static bool kexinit_keyword_found(ptrlen list, ptrlen keyword)
{
for (ptrlen word; get_commasep_word(&list, &word) ;)
if (ptrlen_eq_ptrlen(word, keyword))
return true;
return false;
}
typedef struct ScanKexinitsResult {
bool success;
/* only if success is false */
enum {
SKR_INCOMPLETE,
SKR_UNKNOWN_ID,
SKR_NO_AGREEMENT,
} error;
const char *kind; /* what kind of thing did we fail to sort out? */
ptrlen desc; /* and what was it? or what was the available list? */
} ScanKexinitsResult;
static ScanKexinitsResult ssh2_scan_kexinits(
ptrlen client_kexinit, ptrlen server_kexinit, bool we_are_server,
struct kexinit_algorithm_list kexlists[NKEXLIST],
const ssh_kex **kex_alg, const ssh_keyalg **hostkey_alg,
transport_direction *cs, transport_direction *sc,
bool *warn_kex, bool *warn_hk, bool *warn_cscipher, bool *warn_sccipher,
Ssh *ssh, bool *ignore_guess_cs_packet, bool *ignore_guess_sc_packet,
bool *ignore_guess_cs_packet, bool *ignore_guess_sc_packet,
struct server_hostkeys *server_hostkeys, unsigned *hkflags,
bool *can_send_ext_info)
bool *can_send_ext_info, bool first_time, bool *strict_kex)
{
BinarySource client[1], server[1];
int i;
@ -990,11 +1055,10 @@ static bool ssh2_scan_kexinits(
clists[i] = get_string(client);
slists[i] = get_string(server);
if (get_err(client) || get_err(server)) {
/* Report a better error than the spurious "Couldn't
* agree" that we'd generate if we pressed on regardless
* and treated the empty get_string() result as genuine */
ssh_proto_error(ssh, "KEXINIT packet was incomplete");
return false;
ScanKexinitsResult skr = {
.success = false, .error = SKR_INCOMPLETE,
};
return skr;
}
for (cfirst = true, clist = clists[i];
@ -1042,10 +1106,11 @@ static bool ssh2_scan_kexinits(
* produce a reasonably useful message instead of an
* assertion failure.
*/
ssh_sw_abort(ssh, "Selected %s \"%.*s\" does not correspond to "
"any supported algorithm",
kexlist_descr[i], PTRLEN_PRINTF(found));
return false;
ScanKexinitsResult skr = {
.success = false, .error = SKR_UNKNOWN_ID,
.kind = kexlist_descr[i], .desc = found,
};
return skr;
}
/*
@ -1100,9 +1165,11 @@ static bool ssh2_scan_kexinits(
/*
* Otherwise, any match failure _is_ a fatal error.
*/
ssh_sw_abort(ssh, "Couldn't agree a %s (available: %.*s)",
kexlist_descr[i], PTRLEN_PRINTF(slists[i]));
return false;
ScanKexinitsResult skr = {
.success = false, .error = SKR_UNKNOWN_ID,
.kind = kexlist_descr[i], .desc = slists[i],
};
return skr;
}
switch (i) {
@ -1165,16 +1232,18 @@ static bool ssh2_scan_kexinits(
/*
* Check whether the other side advertised support for EXT_INFO.
*/
{
ptrlen extinfo_advert =
(server_hostkeys ? PTRLEN_LITERAL("ext-info-c") :
PTRLEN_LITERAL("ext-info-s"));
ptrlen list = (server_hostkeys ? clists[KEXLIST_KEX] :
slists[KEXLIST_KEX]);
for (ptrlen word; get_commasep_word(&list, &word) ;)
if (ptrlen_eq_ptrlen(word, extinfo_advert))
*can_send_ext_info = true;
}
if (kexinit_keyword_found(
we_are_server ? clists[KEXLIST_KEX] : slists[KEXLIST_KEX],
we_are_server ? ext_info_c : ext_info_s))
*can_send_ext_info = true;
/*
* Check whether the other side advertised support for kex-strict.
*/
if (first_time && kexinit_keyword_found(
we_are_server ? clists[KEXLIST_KEX] : slists[KEXLIST_KEX],
we_are_server ? kex_strict_c : kex_strict_s))
*strict_kex = true;
if (server_hostkeys) {
/*
@ -1196,7 +1265,33 @@ static bool ssh2_scan_kexinits(
}
}
return true;
ScanKexinitsResult skr = { .success = true };
return skr;
}
static void ssh2_report_scan_kexinits_error(Ssh *ssh, ScanKexinitsResult skr)
{
assert(!skr.success);
switch (skr.error) {
case SKR_INCOMPLETE:
/* Report a better error than the spurious "Couldn't
* agree" that we'd generate if we pressed on regardless
* and treated the empty get_string() result as genuine */
ssh_proto_error(ssh, "KEXINIT packet was incomplete");
break;
case SKR_UNKNOWN_ID:
ssh_sw_abort(ssh, "Selected %s \"%.*s\" does not correspond to "
"any supported algorithm",
skr.kind, PTRLEN_PRINTF(skr.desc));
break;
case SKR_NO_AGREEMENT:
ssh_sw_abort(ssh, "Couldn't agree a %s (available: %.*s)",
skr.kind, PTRLEN_PRINTF(skr.desc));
break;
default:
unreachable("bad ScanKexinitsResult");
}
}
static inline bool delay_outgoing_kexinit(struct ssh2_transport_state *s)
@ -1239,10 +1334,26 @@ static void filter_outgoing_kexinit(struct ssh2_transport_state *s)
strbuf_clear(out);
ptrlen olist = get_string(osrc), ilist = get_string(isrc);
for (ptrlen oword; get_commasep_word(&olist, &oword) ;) {
ptrlen searchword = oword;
ptrlen ilist_copy = ilist;
/*
* Special case: the kex_strict keywords are
* asymmetrically named, so if we're contemplating
* including one of them in our filtered KEXINIT, we
* should search the other side's KEXINIT for the _other_
* one, not the same one.
*/
if (i == KEXLIST_KEX) {
if (ptrlen_eq_ptrlen(oword, kex_strict_c))
searchword = kex_strict_s;
else if (ptrlen_eq_ptrlen(oword, kex_strict_s))
searchword = kex_strict_c;
}
bool add = false;
for (ptrlen iword; get_commasep_word(&ilist_copy, &iword) ;) {
if (ptrlen_eq_ptrlen(oword, iword)) {
if (ptrlen_eq_ptrlen(searchword, iword)) {
/* Found this word in the incoming list. */
add = true;
break;
@ -1461,15 +1572,32 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
{
struct server_hostkeys hks = { NULL, 0, 0 };
if (!ssh2_scan_kexinits(
ScanKexinitsResult skr = ssh2_scan_kexinits(
ptrlen_from_strbuf(s->client_kexinit),
ptrlen_from_strbuf(s->server_kexinit),
ptrlen_from_strbuf(s->server_kexinit), s->ssc != NULL,
s->kexlists, &s->kex_alg, &s->hostkey_alg, s->cstrans,
s->sctrans, &s->warn_kex, &s->warn_hk, &s->warn_cscipher,
&s->warn_sccipher, s->ppl.ssh, NULL, &s->ignorepkt, &hks,
&s->hkflags, &s->can_send_ext_info)) {
&s->warn_sccipher, NULL, &s->ignorepkt, &hks,
&s->hkflags, &s->can_send_ext_info, !s->got_session_id,
&s->strict_kex);
if (!skr.success) {
sfree(hks.indices);
return; /* false means a fatal error function was called */
ssh2_report_scan_kexinits_error(s->ppl.ssh, skr);
return; /* we just called a fatal error function */
}
/*
* If we've just turned on strict kex mode, say so, and
* retrospectively fault any pre-KEXINIT extraneous packets.
*/
if (!s->got_session_id && s->strict_kex) {
ppl_logevent("Enabling strict key exchange semantics");
if (s->seen_non_kexinit) {
ssh_proto_error(s->ppl.ssh, "Received a packet before KEXINIT "
"in strict-kex mode");
return;
}
}
/*
@ -1499,7 +1627,8 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
if (s->warn_kex) {
s->spr = ssh2_transport_confirm_weak_crypto_primitive(
s, "key-exchange algorithm", s->kex_alg->name, s->kex_alg);
s, "key-exchange algorithm", s->kex_alg->name, s->kex_alg,
WCR_BELOW_THRESHOLD);
crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
if (spr_is_abort(s->spr)) {
ssh_spr_close(s->ppl.ssh, s->spr, "kex warning");
@ -1509,7 +1638,8 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
if (s->warn_hk) {
int j, k;
char *betteralgs;
const char **betteralgs = NULL;
size_t nbetter = 0, bettersize = 0;
/*
* Change warning box wording depending on why we chose a
@ -1518,7 +1648,6 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
* could usefully cross-certify. Otherwise, use the same
* standard wording as any other weak crypto primitive.
*/
betteralgs = NULL;
for (j = 0; j < s->n_uncert_hostkeys; j++) {
const struct ssh_signkey_with_user_pref_id *hktype =
&ssh2_hostkey_algs[s->uncert_hostkeys[j]];
@ -1533,19 +1662,16 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
}
}
if (better) {
if (betteralgs) {
char *old_ba = betteralgs;
betteralgs = dupcat(betteralgs, ",", hktype->alg->ssh_id);
sfree(old_ba);
} else {
betteralgs = dupstr(hktype->alg->ssh_id);
}
sgrowarray(betteralgs, bettersize, nbetter);
betteralgs[nbetter++] = hktype->alg->ssh_id;
}
}
if (betteralgs) {
/* Use the special warning prompt that lets us provide
* a list of better algorithms */
s->spr = seat_confirm_weak_cached_hostkey(
sgrowarray(betteralgs, bettersize, nbetter);
betteralgs[nbetter] = NULL;
s->spr = confirm_weak_cached_hostkey(
ppl_get_iseat(&s->ppl), s->hostkey_alg->ssh_id, betteralgs,
ssh2_transport_dialog_callback, s);
sfree(betteralgs);
@ -1554,7 +1680,7 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
* warning prompt */
s->spr = ssh2_transport_confirm_weak_crypto_primitive(
s, "host key type", s->hostkey_alg->ssh_id,
s->hostkey_alg);
s->hostkey_alg, WCR_BELOW_THRESHOLD);
}
crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
if (spr_is_abort(s->spr)) {
@ -1566,7 +1692,7 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
if (s->warn_cscipher) {
s->spr = ssh2_transport_confirm_weak_crypto_primitive(
s, "client-to-server cipher", s->out.cipher->ssh2_id,
s->out.cipher);
s->out.cipher, WCR_BELOW_THRESHOLD);
crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
if (spr_is_abort(s->spr)) {
ssh_spr_close(s->ppl.ssh, s->spr, "cipher warning");
@ -1577,7 +1703,7 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
if (s->warn_sccipher) {
s->spr = ssh2_transport_confirm_weak_crypto_primitive(
s, "server-to-client cipher", s->in.cipher->ssh2_id,
s->in.cipher);
s->in.cipher, WCR_BELOW_THRESHOLD);
crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
if (spr_is_abort(s->spr)) {
ssh_spr_close(s->ppl.ssh, s->spr, "cipher warning");
@ -1585,6 +1711,46 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
}
}
{
s->terrapin.csvuln = terrapin_vulnerable(s->strict_kex, s->cstrans);
s->terrapin.scvuln = terrapin_vulnerable(s->strict_kex, s->sctrans);
s->terrapin.wcr = WCR_TERRAPIN;
if (s->terrapin.csvuln || s->terrapin.scvuln) {
ppl_logevent("SSH connection is vulnerable to 'Terrapin' attack "
"(CVE-2023-48795)");
if (try_to_avoid_terrapin(s))
s->terrapin.wcr = WCR_TERRAPIN_AVOIDABLE;
}
if (s->terrapin.csvuln) {
s->spr = ssh2_transport_confirm_weak_crypto_primitive(
s, "client-to-server cipher", s->terrapin.csvuln,
terrapin_weakness, s->terrapin.wcr);
crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
if (spr_is_abort(s->spr)) {
ssh_spr_close(s->ppl.ssh, s->spr, "vulnerability warning");
return;
}
}
if (s->terrapin.scvuln) {
s->spr = ssh2_transport_confirm_weak_crypto_primitive(
s, "server-to-client cipher", s->terrapin.scvuln,
terrapin_weakness, s->terrapin.wcr);
crMaybeWaitUntilV(s->spr.kind != SPRK_INCOMPLETE);
if (spr_is_abort(s->spr)) {
ssh_spr_close(s->ppl.ssh, s->spr, "vulnerability warning");
return;
}
}
if (s->terrapin.csvuln || s->terrapin.scvuln) {
ppl_logevent("Continuing despite 'Terrapin' vulnerability, "
"at user request");
}
}
/*
* If the other side has sent an initial key exchange packet that
* we must treat as a wrong guess, wait for it, and discard it.
@ -1667,7 +1833,9 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
s->ppl.bpp,
s->out.cipher, cipher_key->u, cipher_iv->u,
s->out.mac, s->out.etm_mode, mac_key->u,
s->out.comp, s->out.comp_delayed);
s->out.comp, s->out.comp_delayed,
s->strict_kex);
s->enabled_outgoing_crypto = true;
strbuf_free(cipher_key);
strbuf_free(cipher_iv);
@ -1759,7 +1927,9 @@ static void ssh2_transport_process_queue(PacketProtocolLayer *ppl)
s->ppl.bpp,
s->in.cipher, cipher_key->u, cipher_iv->u,
s->in.mac, s->in.etm_mode, mac_key->u,
s->in.comp, s->in.comp_delayed);
s->in.comp, s->in.comp_delayed,
s->strict_kex);
s->enabled_incoming_crypto = true;
strbuf_free(cipher_key);
strbuf_free(cipher_iv);
@ -2384,20 +2554,21 @@ static int ca_blob_compare(void *av, void *bv)
}
/*
* Wrapper on seat_confirm_weak_crypto_primitive(), which uses the
* Wrapper on confirm_weak_crypto_primitive(), which uses the
* tree234 s->weak_algorithms_consented_to to ensure we ask at most
* once about any given crypto primitive.
*/
static SeatPromptResult ssh2_transport_confirm_weak_crypto_primitive(
struct ssh2_transport_state *s, const char *type, const char *name,
const void *alg)
const void *alg, WeakCryptoReason wcr)
{
if (find234(s->weak_algorithms_consented_to, (void *)alg, NULL))
return SPR_OK;
add234(s->weak_algorithms_consented_to, (void *)alg);
return seat_confirm_weak_crypto_primitive(
ppl_get_iseat(&s->ppl), type, name, ssh2_transport_dialog_callback, s);
return confirm_weak_crypto_primitive(
ppl_get_iseat(&s->ppl), type, name, ssh2_transport_dialog_callback,
s, wcr);
}
static size_t ssh2_transport_queued_data_size(PacketProtocolLayer *ppl)
@ -2416,3 +2587,167 @@ static void ssh2_transport_final_output(PacketProtocolLayer *ppl)
ssh_ppl_final_output(s->higher_layer);
}
/* Check the settings for a transport direction to see if they're
* vulnerable to the Terrapin attack, aka CVE-2023-48795. If so,
* return a string describing the vulnerable thing. */
static const char *terrapin_vulnerable(
bool strict_kex, const transport_direction *d)
{
/*
* Strict kex mode eliminates the vulnerability. (That's what it's
* for.)
*/
if (strict_kex)
return NULL;
/*
* ChaCha20-Poly1305 is vulnerable and perfectly exploitable.
*/
if (d->cipher == &ssh2_chacha20_poly1305)
return "ChaCha20-Poly1305";
/*
* CBC-mode ciphers with OpenSSH's ETM modification are vulnerable
* and probabilistically exploitable.
*/
if (d->etm_mode && (d->cipher->flags & SSH_CIPHER_IS_CBC))
return "a CBC-mode cipher in OpenSSH ETM mode";
return NULL;
}
/*
* Called when we've detected that at least one transport direction
* has the Terrapin vulnerability.
*
* Before we report it, try to replay what would have happened if the
* user had reconfigured their cipher settings to demote
* ChaCha20+Poly1305 to below the warning threshold. If that would
* have avoided the vulnerability, we should say so in the dialog box.
*
* This is basically the only change in PuTTY's configuration that has
* a chance of avoiding the problem. Terrapin affects the modified
* binary packet protocol used with ChaCha20+Poly1305, and also
* CBC-mode ciphers in ETM mode. But PuTTY unconditionally offers the
* ETM mode of each MAC _after_ the non-ETM mode. So the latter case
* can only come up if the server has been configured to _only_ permit
* the ETM modes of those MACs, which means there's nothing we can do
* anyway.
*/
static bool try_to_avoid_terrapin(const struct ssh2_transport_state *s)
{
bool avoidable = false;
strbuf *alt_client_kexinit = strbuf_new();
Conf *alt_conf = conf_copy(s->conf);
struct kexinit_algorithm_list alt_kexlists[NKEXLIST];
memset(alt_kexlists, 0, sizeof(alt_kexlists));
/*
* We only bother doing this if we're the client, because Uppity
* can't present a dialog box anyway.
*/
if (s->ssc)
goto out;
/*
* Demote CIPHER_CHACHA20 to just below CIPHER_WARN, if it was
* previously above it. If not, don't do anything - we don't want
* to _promote_ it.
*/
int ccp_pos_now = -1, ccp_pos_wanted = -1;
for (int i = 0; i < CIPHER_MAX; i++) {
switch (conf_get_int_int(alt_conf, CONF_ssh_cipherlist,
i)) {
case CIPHER_CHACHA20:
ccp_pos_now = i;
break;
case CIPHER_WARN:
ccp_pos_wanted = i;
break;
}
}
if (ccp_pos_now < 0 || ccp_pos_wanted < 0)
goto out; /* shouldn't ever happen: didn't find the two entries */
if (ccp_pos_now >= ccp_pos_wanted)
goto out; /* ChaCha20 is already demoted and it didn't help */
while (ccp_pos_now < ccp_pos_wanted) {
int cnext = conf_get_int_int(alt_conf, CONF_ssh_cipherlist,
ccp_pos_now + 1);
conf_set_int_int(alt_conf, CONF_ssh_cipherlist,
ccp_pos_now, cnext);
ccp_pos_now++;
}
conf_set_int_int(alt_conf, CONF_ssh_cipherlist,
ccp_pos_now + 1, CIPHER_CHACHA20);
/*
* Make the outgoing KEXINIT we would have made using this
* configuration.
*/
put_byte(alt_client_kexinit, SSH2_MSG_KEXINIT);
put_padding(alt_client_kexinit, 16, 'x'); /* fake random padding */
ssh2_write_kexinit_lists(
BinarySink_UPCAST(alt_client_kexinit), alt_kexlists, alt_conf,
s->ssc, s->ppl.remote_bugs, 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);
put_bool(alt_client_kexinit, false); /* guess packet follows */
put_uint32(alt_client_kexinit, 0); /* reserved */
/*
* Re-analyse the incoming KEXINIT with respect to this one, to
* see what we'd have decided on.
*/
transport_direction cstrans, sctrans;
bool warn_kex, warn_hk, warn_cscipher, warn_sccipher;
bool can_send_ext_info = false, strict_kex = false;
unsigned hkflags;
const ssh_kex *kex_alg;
const ssh_keyalg *hostkey_alg;
ScanKexinitsResult skr = ssh2_scan_kexinits(
ptrlen_from_strbuf(alt_client_kexinit),
ptrlen_from_strbuf(s->server_kexinit),
s->ssc != NULL, alt_kexlists, &kex_alg, &hostkey_alg,
&cstrans, &sctrans,
&warn_kex, &warn_hk, &warn_cscipher, &warn_sccipher, NULL, NULL, NULL,
&hkflags, &can_send_ext_info, !s->got_session_id, &strict_kex);
if (!skr.success) /* something else would have gone wrong */
goto out;
/*
* Reject this as an alternative solution if any of the warn flags
* has got worse, or if there's still anything
* Terrapin-vulnerable.
*/
if (warn_kex > s->warn_kex)
goto out;
if (warn_hk > s->warn_hk)
goto out;
if (warn_cscipher > s->warn_cscipher)
goto out;
if (warn_sccipher > s->warn_sccipher)
goto out;
if (terrapin_vulnerable(strict_kex, &cstrans))
goto out;
if (terrapin_vulnerable(strict_kex, &sctrans))
goto out;
/*
* Success! The vulnerability could have been avoided by this Conf
* tweak, and we should tell the user so.
*/
avoidable = true;
out:
for (size_t i = 0; i < NKEXLIST; i++)
sfree(alt_kexlists[i].algs);
strbuf_free(alt_client_kexinit);
conf_free(alt_conf);
return avoidable;
}

View File

@ -180,6 +180,10 @@ struct ssh2_transport_state {
int nbits, pbits;
bool warn_kex, warn_hk, warn_cscipher, warn_sccipher;
struct {
const char *csvuln, *scvuln;
WeakCryptoReason wcr;
} terrapin;
mp_int *p, *g, *e, *f;
strbuf *ebuf, *fbuf;
strbuf *kex_shared_secret;
@ -202,6 +206,8 @@ struct ssh2_transport_state {
bool warned_about_no_gss_transient_hostkey;
bool got_session_id;
bool can_send_ext_info, post_newkeys_ext_info;
bool strict_kex, enabled_outgoing_crypto, enabled_incoming_crypto;
bool seen_non_kexinit;
SeatPromptResult spr;
bool guessok;
bool ignorepkt;