diff --git a/Buildscr b/Buildscr index a260c833..cac53536 100644 --- a/Buildscr +++ b/Buildscr @@ -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 diff --git a/LATEST.VER b/LATEST.VER index 5827526e..885b0568 100644 --- a/LATEST.VER +++ b/LATEST.VER @@ -1 +1 @@ -0.79 +0.80 diff --git a/console.c b/console.c index c817c85e..1175c2c4 100644 --- a/console.c +++ b/console.c @@ -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; } diff --git a/crypto/aes-select.c b/crypto/aes-select.c index 62b4ab01..b4daeed1 100644 --- a/crypto/aes-select.c +++ b/crypto/aes-select.c @@ -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, }; diff --git a/doc/plink.but b/doc/plink.but index e91a295a..96d78c10 100644 --- a/doc/plink.but +++ b/doc/plink.but @@ -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: diff --git a/doc/pscp.but b/doc/pscp.but index e788ffde..2d7f022f 100644 --- a/doc/pscp.but +++ b/doc/pscp.but @@ -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 diff --git a/proxy/sshproxy.c b/proxy/sshproxy.c index 8a902e08..8a20d0ac 100644 --- a/proxy/sshproxy.c +++ b/proxy/sshproxy.c @@ -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"); } diff --git a/putty.h b/putty.h index 9a3aeb6e..2f6e40f8 100644 --- a/putty.h +++ b/putty.h @@ -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); diff --git a/ssh.h b/ssh.h index 8eed0662..389a6583 100644 --- a/ssh.h +++ b/ssh.h @@ -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); diff --git a/ssh/bpp.h b/ssh/bpp.h index 87e7d7e7..23af5236 100644 --- a/ssh/bpp.h +++ b/ssh/bpp.h @@ -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 diff --git a/ssh/bpp2.c b/ssh/bpp2.c index e019dd2e..88003e82 100644 --- a/ssh/bpp2.c +++ b/ssh/bpp2.c @@ -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); diff --git a/ssh/common.c b/ssh/common.c index ad2d4632..5142e102 100644 --- a/ssh/common.c +++ b/ssh/common.c @@ -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. */ diff --git a/ssh/login1.c b/ssh/login1.c index aa731848..808630ed 100644 --- a/ssh/login1.c +++ b/ssh/login1.c @@ -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"); diff --git a/ssh/server.c b/ssh/server.c index d3a129fa..b477392e 100644 --- a/ssh/server.c +++ b/ssh/server.c @@ -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; } diff --git a/ssh/transport2.c b/ssh/transport2.c index d1c255fc..5dd73cfe 100644 --- a/ssh/transport2.c +++ b/ssh/transport2.c @@ -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; +} diff --git a/ssh/transport2.h b/ssh/transport2.h index 204573fb..ea739df9 100644 --- a/ssh/transport2.h +++ b/ssh/transport2.h @@ -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; diff --git a/stubs/null-seat.c b/stubs/null-seat.c index 80d8b27f..67c25af0 100644 --- a/stubs/null-seat.c +++ b/stubs/null-seat.c @@ -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; } diff --git a/unix/console.c b/unix/console.c index c92ef34b..f3d98309 100644 --- a/unix/console.c +++ b/unix/console.c @@ -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); { diff --git a/unix/dialog.c b/unix/dialog.c index a2296c4d..6c70aaa7 100644 --- a/unix/dialog.c +++ b/unix/dialog.c @@ -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; } diff --git a/unix/platform.h b/unix/platform.h index cf9321f7..7b9a7ccf 100644 --- a/unix/platform.h +++ b/unix/platform.h @@ -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 diff --git a/utils/tempseat.c b/utils/tempseat.c index a8d99cc2..4e4e2b13 100644 --- a/utils/tempseat.c +++ b/utils/tempseat.c @@ -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 " diff --git a/windows/console.c b/windows/console.c index ad86eb5c..53ae8d16 100644 --- a/windows/console.c +++ b/windows/console.c @@ -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; } diff --git a/windows/dialog.c b/windows/dialog.c index 61bcf7ba..e7faf6df 100644 --- a/windows/dialog.c +++ b/windows/dialog.c @@ -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 diff --git a/windows/platform.h b/windows/platform.h index 094c8adc..f0bb5844 100644 --- a/windows/platform.h +++ b/windows/platform.h @@ -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);