1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-03-28 09:17:07 -05:00

Merge tag '0.80'.

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

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
commit 968ac6dbf0
24 changed files with 803 additions and 283 deletions

View File

@ -35,7 +35,7 @@ module putty
ifeq "$(RELEASE)" "" set Ndate $(!builddate)
ifneq "$(Ndate)" "" in . do echo $(Ndate) | perl -pe 's/(....)(..)(..)/$$1-$$2-$$3/' > date
ifneq "$(Ndate)" "" read Date date
set Epoch 18595 # update this at every release
set Epoch 18707 # update this at every release
ifneq "$(Ndate)" "" in . do echo $(Ndate) | perl -ne 'use Time::Local; /(....)(..)(..)/ and print timegm(0,0,0,$$3,$$2-1,$$1) / 86400 - $(Epoch)' > days
ifneq "$(Ndate)" "" read Days days

View File

@ -1 +1 @@
0.79
0.80

View File

@ -9,18 +9,6 @@
#include "misc.h"
#include "console.h"
const char weakcrypto_msg_common_fmt[] =
"The first %s supported by the server is\n"
"%s, which is below the configured warning threshold.\n";
const char weakhk_msg_common_fmt[] =
"The first host key type we have stored for this server\n"
"is %s, which is below the configured warning threshold.\n"
"The server also provides the following types of host key\n"
"above the threshold, which we do not have stored:\n"
"%s\n";
const char console_continue_prompt[] = "Continue with connection? (y/n) ";
const char console_abandoned_msg[] = "Connection abandoned.\n";
const SeatDialogPromptDescriptions *console_prompt_descriptions(Seat *seat)
@ -30,6 +18,8 @@ const SeatDialogPromptDescriptions *console_prompt_descriptions(Seat *seat)
.hk_connect_once_action = "enter \"n\"",
.hk_cancel_action = "press Return",
.hk_cancel_action_Participle = "Pressing Return",
.weak_accept_action = "enter \"y\"",
.weak_cancel_action = "enter \"n\"",
};
return &descs;
}

View File

@ -59,23 +59,26 @@ static ssh_cipher *aes_select(const ssh_cipheralg *alg)
__VA_ARGS__ \
}
AES_SELECTOR_VTABLE(cbc, "aes128-cbc", "CBC", 128, );
AES_SELECTOR_VTABLE(cbc, "aes192-cbc", "CBC", 192, );
AES_SELECTOR_VTABLE(cbc, "aes256-cbc", "CBC", 256, );
AES_SELECTOR_VTABLE(cbc, "aes128-cbc", "CBC", 128, .flags = SSH_CIPHER_IS_CBC);
AES_SELECTOR_VTABLE(cbc, "aes192-cbc", "CBC", 192, .flags = SSH_CIPHER_IS_CBC);
AES_SELECTOR_VTABLE(cbc, "aes256-cbc", "CBC", 256, .flags = SSH_CIPHER_IS_CBC);
AES_SELECTOR_VTABLE(sdctr, "aes128-ctr", "SDCTR", 128, );
AES_SELECTOR_VTABLE(sdctr, "aes192-ctr", "SDCTR", 192, );
AES_SELECTOR_VTABLE(sdctr, "aes256-ctr", "SDCTR", 256, );
AES_SELECTOR_VTABLE(gcm, "aes128-gcm@openssh.com", "GCM", 128,
.required_mac = &ssh2_aesgcm_mac);
.required_mac = &ssh2_aesgcm_mac,
.flags = SSH_CIPHER_SEPARATE_LENGTH);
AES_SELECTOR_VTABLE(gcm, "aes256-gcm@openssh.com", "GCM", 256,
.required_mac = &ssh2_aesgcm_mac);
.required_mac = &ssh2_aesgcm_mac,
.flags = SSH_CIPHER_SEPARATE_LENGTH);
/* 192-bit AES-GCM is included only so that testcrypt can run standard
* test vectors against it. OpenSSH doesn't define a protocol id for
* it. Hence setting its ssh2_id to NULL here, and more importantly,
* leaving it out of aesgcm_list[] below. */
AES_SELECTOR_VTABLE(gcm, NULL, "GCM", 192,
.required_mac = &ssh2_aesgcm_mac);
.required_mac = &ssh2_aesgcm_mac,
.flags = SSH_CIPHER_SEPARATE_LENGTH);
static const ssh_cipheralg ssh_rijndael_lysator = {
/* Same as aes256_cbc, but with a different protocol ID */
@ -84,6 +87,7 @@ static const ssh_cipheralg ssh_rijndael_lysator = {
.blksize = 16,
.real_keybits = 256,
.padded_keybytes = 256/8,
.flags = SSH_CIPHER_IS_CBC,
.text_name = "AES-256 CBC (dummy selector vtable)",
.extra = ssh_aes256_cbc_impls,
};

View File

@ -41,7 +41,7 @@ use Plink:
\c C:\>plink
\c Plink: command-line connection utility
\c Release 0.79
\c Release 0.80
\c Usage: plink [options] [user@]host [command]
\c ("host" can also be a PuTTY saved session name)
\c Options:

View File

@ -39,7 +39,7 @@ use PSCP:
\c C:\>pscp
\c PuTTY Secure Copy client
\c Release 0.79
\c Release 0.80
\c Usage: pscp [options] [user@]host:source target
\c pscp [options] source [source...] [user@]host:target
\c pscp [options] -ls [user@]host:filespec

View File

@ -438,8 +438,32 @@ static SeatPromptResult sshproxy_confirm_ssh_host_key(
return SPR_SW_ABORT("Noninteractive SSH proxy cannot confirm host key");
}
static void sshproxy_format_seatdialogtext(strbuf *sb, SeatDialogText *text)
{
for (SeatDialogTextItem *item = text->items,
*end = item+text->nitems; item < end; item++) {
switch (item->type) {
case SDT_SCARY_HEADING:
case SDT_PARA:
case SDT_DISPLAY:
put_stringz(sb, item->text);
put_byte(sb, '\n');
break;
case SDT_BATCH_ABORT:
put_stringz(sb, item->text);
put_byte(sb, '\n');
goto endloop;
default:
break;
}
}
endloop:
while (strbuf_chomp(sb, '\n'));
}
static SeatPromptResult sshproxy_confirm_weak_crypto_primitive(
Seat *seat, const char *algtype, const char *algname,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
SshProxy *sp = container_of(seat, SshProxy, seat);
@ -450,22 +474,24 @@ static SeatPromptResult sshproxy_confirm_weak_crypto_primitive(
* request on to it.
*/
return seat_confirm_weak_crypto_primitive(
wrap(sp->clientseat), algtype, algname, callback, ctx);
wrap(sp->clientseat), text, callback, ctx);
}
/*
* Otherwise, behave as if we're in batch mode: take the safest
* option.
*/
sshproxy_error(sp, "First %s supported by server is %s, below warning "
"threshold. Abandoning proxy SSH connection.",
algtype, algname);
strbuf *sb = strbuf_new();
sshproxy_format_seatdialogtext(sb, text);
sshproxy_error(sp, sb->s);
strbuf_free(sb);
return SPR_SW_ABORT("Noninteractive SSH proxy cannot confirm "
"weak crypto primitive");
}
static SeatPromptResult sshproxy_confirm_weak_cached_hostkey(
Seat *seat, const char *algname, const char *betteralgs,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
SshProxy *sp = container_of(seat, SshProxy, seat);
@ -476,16 +502,18 @@ static SeatPromptResult sshproxy_confirm_weak_cached_hostkey(
* request on to it.
*/
return seat_confirm_weak_cached_hostkey(
wrap(sp->clientseat), algname, betteralgs, callback, ctx);
wrap(sp->clientseat), text, callback, ctx);
}
/*
* Otherwise, behave as if we're in batch mode: take the safest
* option.
*/
sshproxy_error(sp, "First host key type stored for server is %s, below "
"warning threshold. Abandoning proxy SSH connection.",
algname);
strbuf *sb = strbuf_new();
sshproxy_format_seatdialogtext(sb, text);
sshproxy_error(sp, sb->s);
strbuf_free(sb);
return SPR_SW_ABORT("Noninteractive SSH proxy cannot confirm "
"weak cached host key");
}

24
putty.h
View File

@ -1284,7 +1284,7 @@ struct SeatVtable {
* confirm_ssh_host_key above.
*/
SeatPromptResult (*confirm_weak_crypto_primitive)(
Seat *seat, const char *algtype, const char *algname,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
/*
@ -1295,11 +1295,10 @@ struct SeatVtable {
* This form is used in the case where we're using a host key
* below the warning threshold because that's the best one we have
* cached, but at least one host key algorithm *above* the
* threshold is available that we don't have cached. 'betteralgs'
* lists the better algorithm(s).
* threshold is available that we don't have cached.
*/
SeatPromptResult (*confirm_weak_cached_hostkey)(
Seat *seat, const char *algname, const char *betteralgs,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
/*
@ -1435,15 +1434,15 @@ static inline SeatPromptResult seat_confirm_ssh_host_key(
{ return iseat.seat->vt->confirm_ssh_host_key(
iseat.seat, h, p, ktyp, kstr, text, helpctx, cb, ctx); }
static inline SeatPromptResult seat_confirm_weak_crypto_primitive(
InteractionReadySeat iseat, const char *atyp, const char *aname,
InteractionReadySeat iseat, SeatDialogText *text,
void (*cb)(void *ctx, SeatPromptResult result), void *ctx)
{ return iseat.seat->vt->confirm_weak_crypto_primitive(
iseat.seat, atyp, aname, cb, ctx); }
iseat.seat, text, cb, ctx); }
static inline SeatPromptResult seat_confirm_weak_cached_hostkey(
InteractionReadySeat iseat, const char *aname, const char *better,
InteractionReadySeat iseat, SeatDialogText *text,
void (*cb)(void *ctx, SeatPromptResult result), void *ctx)
{ return iseat.seat->vt->confirm_weak_cached_hostkey(
iseat.seat, aname, better, cb, ctx); }
iseat.seat, text, cb, ctx); }
static inline const SeatDialogPromptDescriptions *seat_prompt_descriptions(
Seat *seat)
{ return seat->vt->prompt_descriptions(seat); }
@ -1497,6 +1496,7 @@ struct SeatDialogPromptDescriptions {
const char *hk_accept_action;
const char *hk_connect_once_action;
const char *hk_cancel_action, *hk_cancel_action_Participle;
const char *weak_accept_action, *weak_cancel_action;
};
/* In the utils subdir: print a message to the Seat which can't be
@ -1530,10 +1530,10 @@ SeatPromptResult nullseat_confirm_ssh_host_key(
char *keystr, SeatDialogText *text, HelpCtx helpctx,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
SeatPromptResult nullseat_confirm_weak_crypto_primitive(
Seat *seat, const char *algtype, const char *algname,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
SeatPromptResult nullseat_confirm_weak_cached_hostkey(
Seat *seat, const char *algname, const char *betteralgs,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
const SeatDialogPromptDescriptions *nullseat_prompt_descriptions(Seat *seat);
bool nullseat_is_never_utf8(Seat *seat);
@ -1567,10 +1567,10 @@ SeatPromptResult console_confirm_ssh_host_key(
char *keystr, SeatDialogText *text, HelpCtx helpctx,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
SeatPromptResult console_confirm_weak_crypto_primitive(
Seat *seat, const char *algtype, const char *algname,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
SeatPromptResult console_confirm_weak_cached_hostkey(
Seat *seat, const char *algname, const char *betteralgs,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
StripCtrlChars *console_stripctrl_new(
Seat *seat, BinarySink *bs_out, SeatInteractionContext sic);

15
ssh.h
View File

@ -1903,11 +1903,26 @@ void add_to_commasep(strbuf *buf, const char *data);
void add_to_commasep_pl(strbuf *buf, ptrlen data);
bool get_commasep_word(ptrlen *list, ptrlen *word);
/* Reasons why something warned by confirm_weak_crypto_primitive might
* be considered weak */
typedef enum WeakCryptoReason {
WCR_BELOW_THRESHOLD, /* user has told us to consider it weak */
WCR_TERRAPIN, /* known vulnerability CVE-2023-48795 */
WCR_TERRAPIN_AVOIDABLE, /* same, but demoting ChaCha20 can avoid it */
} WeakCryptoReason;
SeatPromptResult verify_ssh_host_key(
InteractionReadySeat iseat, Conf *conf, const char *host, int port,
ssh_key *key, const char *keytype, char *keystr, const char *keydisp,
char **fingerprints, int ca_count,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
SeatPromptResult confirm_weak_crypto_primitive(
InteractionReadySeat iseat, const char *algtype, const char *algname,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx,
WeakCryptoReason wcr);
SeatPromptResult confirm_weak_cached_hostkey(
InteractionReadySeat iseat, const char *algname, const char **betteralgs,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
typedef struct ssh_transient_hostkey_cache ssh_transient_hostkey_cache;
ssh_transient_hostkey_cache *ssh_transient_hostkey_cache_new(void);

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;

View File

@ -27,11 +27,11 @@ SeatPromptResult nullseat_confirm_ssh_host_key(
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{ return SPR_SW_ABORT("this seat can't handle interactive prompts"); }
SeatPromptResult nullseat_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_SW_ABORT("this seat can't handle interactive prompts"); }
SeatPromptResult nullseat_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_SW_ABORT("this seat can't handle interactive prompts"); }
bool nullseat_is_never_utf8(Seat *seat) { return false; }
@ -61,6 +61,8 @@ const SeatDialogPromptDescriptions *nullseat_prompt_descriptions(Seat *seat)
.hk_connect_once_action = "",
.hk_cancel_action = "",
.hk_cancel_action_Participle = "",
.weak_accept_action = "",
.weak_cancel_action = "",
};
return &descs;
}

View File

@ -102,20 +102,18 @@ static int block_and_read(int fd, void *buf, size_t len)
return ret;
}
SeatPromptResult console_confirm_ssh_host_key(
Seat *seat, const char *host, int port, const char *keytype,
char *keystr, SeatDialogText *text, HelpCtx helpctx,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
/*
* Helper function to print the message from a SeatDialogText. Returns
* the final prompt to print on the input line, or NULL if a
* batch-mode abort is needed. In the latter case it will have printed
* the abort text already.
*/
static const char *console_print_seatdialogtext(SeatDialogText *text)
{
char line[32];
struct termios cf;
const char *prompt = NULL;
stdio_sink errsink[1];
stdio_sink_init(errsink, stderr);
premsg(&cf);
for (SeatDialogTextItem *item = text->items,
*end = item+text->nitems; item < end; item++) {
switch (item->type) {
@ -135,8 +133,7 @@ SeatPromptResult console_confirm_ssh_host_key(
if (console_batch_mode) {
fprintf(stderr, "%s\n", item->text);
fflush(stderr);
postmsg(&cf);
return SPR_SW_ABORT("Cannot confirm a host key in batch mode");
return NULL;
}
break;
case SDT_PROMPT:
@ -146,7 +143,26 @@ SeatPromptResult console_confirm_ssh_host_key(
break;
}
}
assert(prompt); /* something in the SeatDialogText should have set this */
return prompt;
}
SeatPromptResult console_confirm_ssh_host_key(
Seat *seat, const char *host, int port, const char *keytype,
char *keystr, SeatDialogText *text, HelpCtx helpctx,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
char line[32];
struct termios cf;
premsg(&cf);
const char *prompt = console_print_seatdialogtext(text);
if (!prompt) {
postmsg(&cf);
return SPR_SW_ABORT("Cannot confirm a host key in batch mode");
}
while (true) {
fprintf(stderr,
@ -202,23 +218,22 @@ SeatPromptResult console_confirm_ssh_host_key(
}
SeatPromptResult console_confirm_weak_crypto_primitive(
Seat *seat, const char *algtype, const char *algname,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
char line[32];
struct termios cf;
premsg(&cf);
fprintf(stderr, weakcrypto_msg_common_fmt, algtype, algname);
if (console_batch_mode) {
fputs(console_abandoned_msg, stderr);
const char *prompt = console_print_seatdialogtext(text);
if (!prompt) {
postmsg(&cf);
return SPR_SW_ABORT("Cannot confirm a weak crypto primitive "
"in batch mode");
}
fputs(console_continue_prompt, stderr);
fprintf(stderr, "%s (y/n) ", prompt);
fflush(stderr);
{
@ -244,23 +259,22 @@ SeatPromptResult console_confirm_weak_crypto_primitive(
}
SeatPromptResult console_confirm_weak_cached_hostkey(
Seat *seat, const char *algname, const char *betteralgs,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
char line[32];
struct termios cf;
premsg(&cf);
fprintf(stderr, weakhk_msg_common_fmt, algname, betteralgs);
if (console_batch_mode) {
fputs(console_abandoned_msg, stderr);
const char *prompt = console_print_seatdialogtext(text);
if (!prompt) {
postmsg(&cf);
return SPR_SW_ABORT("Cannot confirm a weak cached host key "
"in batch mode");
}
fputs(console_continue_prompt, stderr);
fprintf(stderr, "%s (y/n) ", prompt);
fflush(stderr);
{

View File

@ -3608,10 +3608,54 @@ const SeatDialogPromptDescriptions *gtk_seat_prompt_descriptions(Seat *seat)
.hk_connect_once_action = "press \"Connect Once\"",
.hk_cancel_action = "press \"Cancel\"",
.hk_cancel_action_Participle = "Pressing \"Cancel\"",
.weak_accept_action = "press \"Yes\"",
.weak_cancel_action = "press \"No\"",
};
return &descs;
}
/*
* Format a SeatDialogText into a strbuf, also adjusting the box width
* to cope with displayed text. Returns the dialog box title.
*/
static const char *gtk_format_seatdialogtext(
SeatDialogText *text, strbuf *dlg_text, int *width)
{
const char *dlg_title = NULL;
for (SeatDialogTextItem *item = text->items,
*end = item + text->nitems; item < end; item++) {
switch (item->type) {
case SDT_PARA:
put_fmt(dlg_text, "%s\n\n", item->text);
break;
case SDT_DISPLAY: {
put_fmt(dlg_text, "%s\n\n", item->text);
int thiswidth = string_width(item->text);
if (*width < thiswidth)
*width = thiswidth;
break;
}
case SDT_SCARY_HEADING:
/* Can't change font size or weight in this context */
put_fmt(dlg_text, "%s\n\n", item->text);
break;
case SDT_TITLE:
dlg_title = item->text;
break;
default:
break;
}
}
/*
* Trim trailing newlines.
*/
while (strbuf_chomp(dlg_text, '\n'));
return dlg_title;
}
SeatPromptResult gtk_seat_confirm_ssh_host_key(
Seat *seat, const char *host, int port, const char *keytype,
char *keystr, SeatDialogText *text, HelpCtx helpctx,
@ -3626,35 +3670,9 @@ SeatPromptResult gtk_seat_confirm_ssh_host_key(
button_array_hostkey, lenof(button_array_hostkey),
};
const char *dlg_title = NULL;
strbuf *dlg_text = strbuf_new();
int width = string_width("default dialog width determination string");
for (SeatDialogTextItem *item = text->items,
*end = item + text->nitems; item < end; item++) {
switch (item->type) {
case SDT_PARA:
put_fmt(dlg_text, "%s\n\n", item->text);
break;
case SDT_DISPLAY: {
put_fmt(dlg_text, "%s\n\n", item->text);
int thiswidth = string_width(item->text);
if (width < thiswidth)
width = thiswidth;
break;
}
case SDT_SCARY_HEADING:
/* Can't change font size or weight in this context */
put_fmt(dlg_text, "%s\n\n", item->text);
break;
case SDT_TITLE:
dlg_title = item->text;
break;
default:
break;
}
}
while (strbuf_chomp(dlg_text, '\n'));
strbuf *dlg_text = strbuf_new();
const char *dlg_title = gtk_format_seatdialogtext(text, dlg_text, &width);
GtkWidget *mainwin, *msgbox;
@ -3751,19 +3769,16 @@ static void simple_prompt_result_spr_callback(void *vctx, int result)
* below the configured 'warn' threshold).
*/
SeatPromptResult gtk_seat_confirm_weak_crypto_primitive(
Seat *seat, const char *algtype, const char *algname,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
static const char msg[] =
"The first %s supported by the server is "
"%s, which is below the configured warning threshold.\n"
"Continue with connection?";
char *text;
struct simple_prompt_result_spr_ctx *result_ctx;
GtkWidget *mainwin, *msgbox;
text = dupprintf(msg, algtype, algname);
int width = string_width("Reasonably long line of text "
"as a width template");
strbuf *dlg_text = strbuf_new();
const char *dlg_title = gtk_format_seatdialogtext(text, dlg_text, &width);
result_ctx = snew(struct simple_prompt_result_spr_ctx);
result_ctx->callback = callback;
@ -3773,33 +3788,26 @@ SeatPromptResult gtk_seat_confirm_weak_crypto_primitive(
mainwin = GTK_WIDGET(gtk_seat_get_window(seat));
msgbox = create_message_box(
mainwin, "PuTTY Security Alert", text,
string_width("Reasonably long line of text as a width template"),
false, &buttons_yn, simple_prompt_result_spr_callback, result_ctx);
mainwin, dlg_title, dlg_text->s, width, false,
&buttons_yn, simple_prompt_result_spr_callback, result_ctx);
register_dialog(seat, result_ctx->dialog_slot, msgbox);
sfree(text);
strbuf_free(dlg_text);
return SPR_INCOMPLETE;
}
SeatPromptResult gtk_seat_confirm_weak_cached_hostkey(
Seat *seat, const char *algname, const char *betteralgs,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
static const char msg[] =
"The first host key type we have stored for this server\n"
"is %s, which is below the configured warning threshold.\n"
"The server also provides the following types of host key\n"
"above the threshold, which we do not have stored:\n"
"%s\n"
"Continue with connection?";
char *text;
struct simple_prompt_result_spr_ctx *result_ctx;
GtkWidget *mainwin, *msgbox;
text = dupprintf(msg, algname, betteralgs);
int width = string_width("is ecdsa-nistp521, which is below the configured"
" warning threshold.");
strbuf *dlg_text = strbuf_new();
const char *dlg_title = gtk_format_seatdialogtext(text, dlg_text, &width);
result_ctx = snew(struct simple_prompt_result_spr_ctx);
result_ctx->callback = callback;
@ -3809,13 +3817,11 @@ SeatPromptResult gtk_seat_confirm_weak_cached_hostkey(
mainwin = GTK_WIDGET(gtk_seat_get_window(seat));
msgbox = create_message_box(
mainwin, "PuTTY Security Alert", text,
string_width("is ecdsa-nistp521, which is below the configured"
" warning threshold."),
false, &buttons_yn, simple_prompt_result_spr_callback, result_ctx);
mainwin, dlg_title, dlg_text->s, width, false,
&buttons_yn, simple_prompt_result_spr_callback, result_ctx);
register_dialog(seat, result_ctx->dialog_slot, msgbox);
sfree(text);
strbuf_free(dlg_text);
return SPR_INCOMPLETE;
}

View File

@ -229,10 +229,10 @@ SeatPromptResult gtk_seat_confirm_ssh_host_key(
char *keystr, SeatDialogText *text, HelpCtx helpctx,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
SeatPromptResult gtk_seat_confirm_weak_crypto_primitive(
Seat *seat, const char *algtype, const char *algname,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
SeatPromptResult gtk_seat_confirm_weak_cached_hostkey(
Seat *seat, const char *algname, const char *betteralgs,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
const SeatDialogPromptDescriptions *gtk_seat_prompt_descriptions(Seat *seat);
#ifdef MAY_REFER_TO_GTK_IN_HEADERS

View File

@ -255,7 +255,7 @@ static SeatPromptResult tempseat_confirm_ssh_host_key(
}
static SeatPromptResult tempseat_confirm_weak_crypto_primitive(
Seat *seat, const char *algtype, const char *algname,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
unreachable("confirm_weak_crypto_primitive "
@ -263,7 +263,7 @@ static SeatPromptResult tempseat_confirm_weak_crypto_primitive(
}
static SeatPromptResult tempseat_confirm_weak_cached_hostkey(
Seat *seat, const char *algname, const char *betteralgs,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
unreachable("confirm_weak_cached_hostkey "

View File

@ -337,14 +337,16 @@ static ResponseType parse_and_free_response(char *line)
return toret;
}
SeatPromptResult console_confirm_ssh_host_key(
Seat *seat, const char *host, int port, const char *keytype,
char *keystr, SeatDialogText *text, HelpCtx helpctx,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
/*
* Helper function to print the message from a SeatDialogText. Returns
* the final prompt to print on the input line, or NULL if a
* batch-mode abort is needed. In the latter case it will have printed
* the abort text already.
*/
static const char *console_print_seatdialogtext(
ConsoleIO *conio, SeatDialogText *text)
{
ConsoleIO *conio = conio_setup(false);
const char *prompt = NULL;
SeatPromptResult result;
for (SeatDialogTextItem *item = text->items,
*end = item+text->nitems; item < end; item++) {
@ -364,9 +366,7 @@ SeatPromptResult console_confirm_ssh_host_key(
case SDT_BATCH_ABORT:
if (console_batch_mode) {
put_fmt(conio, "%s\n", item->text);
result = SPR_SW_ABORT(
"Cannot confirm a host key in batch mode");
goto out;
return NULL;
}
break;
case SDT_PROMPT:
@ -377,6 +377,20 @@ SeatPromptResult console_confirm_ssh_host_key(
}
}
assert(prompt); /* something in the SeatDialogText should have set this */
return prompt;
}
SeatPromptResult console_confirm_ssh_host_key(
Seat *seat, const char *host, int port, const char *keytype,
char *keystr, SeatDialogText *text, HelpCtx helpctx,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
ConsoleIO *conio = conio_setup(false);
SeatPromptResult result;
const char *prompt = console_print_seatdialogtext(conio, text);
if (!prompt)
return SPR_SW_ABORT("Cannot confirm a host key in batch mode");
ResponseType response;
@ -416,28 +430,24 @@ SeatPromptResult console_confirm_ssh_host_key(
put_dataz(conio, console_abandoned_msg);
result = SPR_USER_ABORT;
}
out:
conio_free(conio);
return result;
}
SeatPromptResult console_confirm_weak_crypto_primitive(
Seat *seat, const char *algtype, const char *algname,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
ConsoleIO *conio = conio_setup(false);
SeatPromptResult result;
put_fmt(conio, weakcrypto_msg_common_fmt, algtype, algname);
const char *prompt = console_print_seatdialogtext(conio, text);
if (!prompt)
return SPR_SW_ABORT("Cannot confirm a weak crypto primitive "
"in batch mode");
if (console_batch_mode) {
put_dataz(conio, console_abandoned_msg);
result = SPR_SW_ABORT("Cannot confirm a weak crypto primitive "
"in batch mode");
goto out;
}
put_dataz(conio, console_continue_prompt);
put_fmt(conio, "%s (y/n)", prompt);
ResponseType response = parse_and_free_response(
console_read_line(conio, true));
@ -448,28 +458,24 @@ SeatPromptResult console_confirm_weak_crypto_primitive(
put_dataz(conio, console_abandoned_msg);
result = SPR_USER_ABORT;
}
out:
conio_free(conio);
return result;
}
SeatPromptResult console_confirm_weak_cached_hostkey(
Seat *seat, const char *algname, const char *betteralgs,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
ConsoleIO *conio = conio_setup(false);
SeatPromptResult result;
put_fmt(conio, weakhk_msg_common_fmt, algname, betteralgs);
const char *prompt = console_print_seatdialogtext(conio, text);
if (!prompt)
return SPR_SW_ABORT("Cannot confirm a weak cached host key "
"in batch mode");
if (console_batch_mode) {
put_dataz(conio, console_abandoned_msg);
result = SPR_SW_ABORT("Cannot confirm a weak cached host key "
"in batch mode");
goto out;
}
put_dataz(conio, console_continue_prompt);
put_fmt(conio, "%s (y/n) ", prompt);
ResponseType response = parse_and_free_response(
console_read_line(conio, true));
@ -480,7 +486,7 @@ SeatPromptResult console_confirm_weak_cached_hostkey(
put_dataz(conio, console_abandoned_msg);
result = SPR_USER_ABORT;
}
out:
conio_free(conio);
return result;
}

View File

@ -949,6 +949,39 @@ static INT_PTR HostKeyMoreInfoProc(HWND hwnd, UINT msg, WPARAM wParam,
return 0;
}
static const char *process_seatdialogtext(
strbuf *dlg_text, const char **scary_heading, SeatDialogText *text)
{
const char *dlg_title = "";
for (SeatDialogTextItem *item = text->items,
*end = item + text->nitems; item < end; item++) {
switch (item->type) {
case SDT_PARA:
put_fmt(dlg_text, "%s\r\n\r\n", item->text);
break;
case SDT_DISPLAY:
put_fmt(dlg_text, "%s\r\n\r\n", item->text);
break;
case SDT_SCARY_HEADING:
assert(scary_heading != NULL && "only expect a scary heading if "
"the dialog has somewhere to put it");
*scary_heading = item->text;
break;
case SDT_TITLE:
dlg_title = item->text;
break;
default:
break;
}
}
/* Trim any trailing newlines */
while (strbuf_chomp(dlg_text, '\r') || strbuf_chomp(dlg_text, '\n'));
return dlg_title;
}
static INT_PTR HostKeyDialogProc(HWND hwnd, UINT msg,
WPARAM wParam, LPARAM lParam, void *vctx)
{
@ -957,32 +990,15 @@ static INT_PTR HostKeyDialogProc(HWND hwnd, UINT msg,
switch (msg) {
case WM_INITDIALOG: {
strbuf *dlg_text = strbuf_new();
const char *dlg_title = "";
ctx->has_title = false;
LPCTSTR iconid = IDI_QUESTION;
const char *scary_heading = NULL;
const char *dlg_title = process_seatdialogtext(
dlg_text, &scary_heading, ctx->text);
for (SeatDialogTextItem *item = ctx->text->items,
*end = item + ctx->text->nitems; item < end; item++) {
switch (item->type) {
case SDT_PARA:
put_fmt(dlg_text, "%s\r\n\r\n", item->text);
break;
case SDT_DISPLAY:
put_fmt(dlg_text, "%s\r\n\r\n", item->text);
break;
case SDT_SCARY_HEADING:
SetDlgItemText(hwnd, IDC_HK_TITLE, item->text);
iconid = IDI_WARNING;
ctx->has_title = true;
break;
case SDT_TITLE:
dlg_title = item->text;
break;
default:
break;
}
LPCTSTR iconid = IDI_QUESTION;
if (scary_heading) {
SetDlgItemText(hwnd, IDC_HK_TITLE, scary_heading);
iconid = IDI_WARNING;
}
while (strbuf_chomp(dlg_text, '\r') || strbuf_chomp(dlg_text, '\n'));
SetDlgItemText(hwnd, IDC_HK_TEXT, dlg_text->s);
MakeDlgItemBorderless(hwnd, IDC_HK_TEXT);
@ -1121,6 +1137,8 @@ const SeatDialogPromptDescriptions *win_seat_prompt_descriptions(Seat *seat)
.hk_connect_once_action = "press \"Connect Once\"",
.hk_cancel_action = "press \"Cancel\"",
.hk_cancel_action_Participle = "Pressing \"Cancel\"",
.weak_accept_action = "press \"Yes\"",
.weak_cancel_action = "press \"No\"",
};
return &descs;
}
@ -1155,25 +1173,17 @@ SeatPromptResult win_seat_confirm_ssh_host_key(
* below the configured 'warn' threshold).
*/
SeatPromptResult win_seat_confirm_weak_crypto_primitive(
Seat *seat, const char *algtype, const char *algname,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
static const char mbtitle[] = "%s Security Alert";
static const char msg[] =
"The first %s supported by the server\n"
"is %s, which is below the configured\n"
"warning threshold.\n"
"Do you want to continue with this connection?\n";
char *message, *title;
int mbret;
strbuf *dlg_text = strbuf_new();
const char *dlg_title = process_seatdialogtext(dlg_text, NULL, text);
message = dupprintf(msg, algtype, algname);
title = dupprintf(mbtitle, appname);
mbret = MessageBox(NULL, message, title,
MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON2);
int mbret = MessageBox(NULL, dlg_text->s, dlg_title,
MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON2);
socket_reselect_all();
sfree(message);
sfree(title);
strbuf_free(dlg_text);
if (mbret == IDYES)
return SPR_OK;
else
@ -1181,27 +1191,17 @@ SeatPromptResult win_seat_confirm_weak_crypto_primitive(
}
SeatPromptResult win_seat_confirm_weak_cached_hostkey(
Seat *seat, const char *algname, const char *betteralgs,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
static const char mbtitle[] = "%s Security Alert";
static const char msg[] =
"The first host key type we have stored for this server\n"
"is %s, which is below the configured warning threshold.\n"
"The server also provides the following types of host key\n"
"above the threshold, which we do not have stored:\n"
"%s\n"
"Do you want to continue with this connection?\n";
char *message, *title;
int mbret;
strbuf *dlg_text = strbuf_new();
const char *dlg_title = process_seatdialogtext(dlg_text, NULL, text);
message = dupprintf(msg, algname, betteralgs);
title = dupprintf(mbtitle, appname);
mbret = MessageBox(NULL, message, title,
MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON2);
int mbret = MessageBox(NULL, dlg_text->s, dlg_title,
MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON2);
socket_reselect_all();
sfree(message);
sfree(title);
strbuf_free(dlg_text);
if (mbret == IDYES)
return SPR_OK;
else

View File

@ -254,10 +254,10 @@ SeatPromptResult win_seat_confirm_ssh_host_key(
char *keystr, SeatDialogText *text, HelpCtx helpctx,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
SeatPromptResult win_seat_confirm_weak_crypto_primitive(
Seat *seat, const char *algtype, const char *algname,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
SeatPromptResult win_seat_confirm_weak_cached_hostkey(
Seat *seat, const char *algname, const char *betteralgs,
Seat *seat, SeatDialogText *text,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
const SeatDialogPromptDescriptions *win_seat_prompt_descriptions(Seat *seat);