From 05afec052cea4de87a8467f9c5583ffe226ce06e Mon Sep 17 00:00:00 2001 From: TJ Saunders Date: Tue, 19 Dec 2023 18:55:58 -0800 Subject: [PATCH] Issue #1760: Implement the "strict KEX" mitigations for the Terrapin SSH protocol attack (CVE-2023-48795). Upstream-Status: Backport [https://github.com/proftpd/proftpd/commit/bcec15efe6c53dac40420731013f1cd2fd54123b] CVE: CVE-2023-48795 Signed-off-by: Vijay Anusuri Signed-off-by: Ankur Tyagi --- contrib/mod_sftp/kex.c | 126 +++++++--- contrib/mod_sftp/mod_sftp.c | 8 +- contrib/mod_sftp/mod_sftp.h.in | 35 +-- contrib/mod_sftp/packet.c | 12 + contrib/mod_sftp/packet.h | 9 +- contrib/mod_sftp/tap.c | 26 ++- contrib/mod_sftp/tap.h | 5 +- doc/contrib/mod_sftp.html | 15 +- tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm | 217 ++++++++++++++++++ 9 files changed, 398 insertions(+), 55 deletions(-) diff --git a/contrib/mod_sftp/kex.c b/contrib/mod_sftp/kex.c index 6a19c88bb..3d9068139 100644 --- a/contrib/mod_sftp/kex.c +++ b/contrib/mod_sftp/kex.c @@ -149,6 +149,13 @@ static struct sftp_kex *kex_first_kex = NULL; static struct sftp_kex *kex_rekey_kex = NULL; static int kex_sent_kexinit = FALSE; +/* Using strict kex? Note that we maintain this value here, rather than + * in the sftp_kex struct, so that any "use strict KEX" flag set via the + * first KEXINIT is used through any subsequent KEXINITs. + */ +static int use_strict_kex = FALSE; +static int kex_done_first_kex = FALSE; + /* Diffie-Hellman group moduli */ static const char *dh_group1_str = @@ -1578,6 +1585,16 @@ static const char *get_kexinit_exchange_list(pool *p) { res = pstrcat(p, res, *res ? "," : "", pstrdup(p, "ext-info-s"), NULL); } + if (!(sftp_opts & SFTP_OPT_NO_STRICT_KEX)) { + /* Indicate support for OpenSSH's custom "strict KEX" mode extension, + * but only if we have not done/completed our first KEX. + */ + if (kex_done_first_kex == FALSE) { + res = pstrcat(p, res, *res ? "," : "", + pstrdup(p, "kex-strict-s-v00@openssh.com"), NULL); + } + } + return res; } @@ -2159,6 +2176,21 @@ static int get_session_names(struct sftp_kex *kex, int *correct_guess) { pr_trace_msg(trace_channel, 20, "client %s EXT_INFO support", kex->use_ext_info ? "signaled" : "did not signal" ); + if (!(sftp_opts & SFTP_OPT_NO_STRICT_KEX)) { + /* Did the client indicate "strict kex" support (Issue 1760)? + * + * Note that we only check for this if it is our first KEXINIT. + * The "strict kex" extension is ignored in any subsequent KEXINITs, as + * for rekeys. + */ + if (kex_done_first_kex == FALSE) { + use_strict_kex = sftp_misc_namelist_contains(kex->pool, + client_list, "kex-strict-c-v00@openssh.com"); + pr_trace_msg(trace_channel, 20, "client %s strict KEX support", + use_strict_kex ? "signaled" : "did not signal" ); + } + } + } else { (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION, "no shared key exchange algorithm found (client sent '%s', server sent " @@ -4391,7 +4423,6 @@ static int handle_kex_ecdh(struct ssh2_packet *pkt, struct sftp_kex *kex) { destroy_pool(pkt->pool); return 0; } - #endif /* PR_USE_OPENSSL_ECC */ static struct ssh2_packet *read_kex_packet(pool *p, struct sftp_kex *kex, @@ -4442,6 +4473,10 @@ static struct ssh2_packet *read_kex_packet(pool *p, struct sftp_kex *kex, /* Per RFC 4253, Section 11, DEBUG, DISCONNECT, IGNORE, and UNIMPLEMENTED * messages can occur at any time, even during KEX. We have to be prepared * for this, and Do The Right Thing(tm). + * + * However, due to the Terrapin attack, if we are using a "strict KEX" + * mode, then only DISCONNECT messages can occur during KEX; DEBUG, + * IGNORE, and UNIMPLEMENTED messages will lead to disconnecting. */ mesg_type = sftp_ssh2_packet_get_mesg_type(pkt); @@ -4469,36 +4504,44 @@ static struct ssh2_packet *read_kex_packet(pool *p, struct sftp_kex *kex, break; } - switch (mesg_type) { - case SFTP_SSH2_MSG_DEBUG: - sftp_ssh2_packet_handle_debug(pkt); - pr_response_set_pool(NULL); - pkt = NULL; - break; - + switch (msg_type) { + /* DISCONNECT messages are always allowed. */ case SFTP_SSH2_MSG_DISCONNECT: sftp_ssh2_packet_handle_disconnect(pkt); pr_response_set_pool(NULL); pkt = NULL; break; + case SFTP_SSH2_MSG_DEBUG: + if (use_strict_kex == FALSE) { + sftp_ssh2_packet_handle_debug(pkt); + pr_response_set_pool(NULL); + pkt = NULL; + break; + } + case SFTP_SSH2_MSG_IGNORE: - sftp_ssh2_packet_handle_ignore(pkt); - pr_response_set_pool(NULL); - pkt = NULL; - break; + if (use_strict_kex == FALSE) { + sftp_ssh2_packet_handle_ignore(pkt); + pr_response_set_pool(NULL); + pkt = NULL; + break; + } case SFTP_SSH2_MSG_UNIMPLEMENTED: - sftp_ssh2_packet_handle_unimplemented(pkt); - pr_response_set_pool(NULL); - pkt = NULL; - break; + if (use_strict_kex == FALSE) { + sftp_ssh2_packet_handle_unimplemented(pkt); + pr_response_set_pool(NULL); + pkt = NULL; + break; + } default: /* For any other message type, it's considered a protocol error. */ (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION, - "received %s (%d) unexpectedly, disconnecting", - sftp_ssh2_packet_get_mesg_type_desc(mesg_type), mesg_type); + "received %s (%d) unexpectedly%s, disconnecting", + sftp_ssh2_packet_get_msg_type_desc(msg_type), msg_type, + use_strict_kex ? " during strict KEX" : ""); pr_response_set_pool(NULL); destroy_kex(kex); destroy_pool(pkt->pool); @@ -4519,7 +4562,7 @@ int sftp_kex_handle(struct ssh2_packet *pkt) { * initial connect (kex_first_kex not null), or because we * are in a server-initiated rekeying (kex_rekey_kex not null). */ - if (kex_first_kex) { + if (kex_first_kex != NULL) { kex = kex_first_kex; /* We need to assign the client/server versions, which this struct @@ -4528,7 +4571,7 @@ int sftp_kex_handle(struct ssh2_packet *pkt) { kex->client_version = kex_client_version; kex->server_version = kex_server_version; - } else if (kex_rekey_kex) { + } else if (kex_rekey_kex != NULL) { kex = kex_rekey_kex; } else { @@ -4564,6 +4607,24 @@ int sftp_kex_handle(struct ssh2_packet *pkt) { return -1; } + if (use_strict_kex == TRUE && + kex_done_first_kex == FALSE) { + uint32_t client_seqno; + + client_seqno = sftp_ssh2_packet_get_client_seqno(); + if (client_seqno != 1) { + /* Receiving any messages other than a KEXINIT as the first client + * message indicates the possibility of the Terrapin attack being + * conducted (Issue 1760). Thus we disconnect the client in such + * cases. + */ + (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION, + "'strict KEX' violation, as KEXINIT was not the first message; disconnecting"); + destroy_kex(kex); + SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL); + } + } + /* Once we have received the client KEXINIT message, we can compare what we * want to send against what we already received from the client. * @@ -4622,7 +4683,7 @@ int sftp_kex_handle(struct ssh2_packet *pkt) { destroy_pool(pkt->pool); - if (!kex_sent_kexinit) { + if (kex_sent_kexinit == FALSE) { pkt = sftp_ssh2_packet_create(kex_pool); res = write_kexinit(pkt, kex); if (res < 0) { @@ -4645,7 +4706,7 @@ int sftp_kex_handle(struct ssh2_packet *pkt) { } } - if (!kex_sent_kexinit) { + if (kex_sent_kexinit == FALSE) { pkt = sftp_ssh2_packet_create(kex_pool); res = write_kexinit(pkt, kex); if (res < 0) { @@ -4770,7 +4831,7 @@ int sftp_kex_handle(struct ssh2_packet *pkt) { NULL, 1, SFTP_SSH2_MSG_NEWKEYS); /* If we didn't send our NEWKEYS message earlier, do it now. */ - if (!sent_newkeys) { + if (sent_newkeys == FALSE) { struct ssh2_packet *pkt2; pr_trace_msg(trace_channel, 9, "sending NEWKEYS message to client"); @@ -4794,6 +4855,11 @@ int sftp_kex_handle(struct ssh2_packet *pkt) { destroy_pool(pkt2->pool); } + if (use_strict_kex == TRUE) { + sftp_ssh2_packet_reset_client_seqno(); + sftp_ssh2_packet_reset_server_seqno(); + } + /* Last but certainly not least, set up the keys for encryption and * authentication, based on H and K. */ @@ -4813,6 +4879,9 @@ int sftp_kex_handle(struct ssh2_packet *pkt) { destroy_pool(pkt->pool); cmd = NULL; + /* We've now completed our KEX, possibly our first. */ + kex_done_first_kex = TRUE; + /* If extension negotiation has not been disabled, AND if we have not * received a service request, AND if the client sent "ext-info-c", THEN * send our EXT_INFO. We do not want send this during rekeys. @@ -4849,6 +4918,12 @@ int sftp_kex_handle(struct ssh2_packet *pkt) { cmd = NULL; } + /* Only start the TAP timer after we have completed our first KEX. + * Otherwise, we risk sending "illegal" packets prior to, or during, + * a "strict KEX" session (Issue 1760). + */ + sftp_tap_start_policy(); + /* Reset this flag for the next time through. */ kex_sent_kexinit = FALSE; @@ -4878,7 +4953,7 @@ int sftp_kex_free(void) { destroy_kex(rekey_kex); } - if (kex_pool) { + if (kex_pool != NULL) { destroy_pool(kex_pool); kex_pool = NULL; } @@ -5050,7 +5125,7 @@ int sftp_kex_send_first_kexinit(void) { struct ssh2_packet *pkt; int res; - if (!kex_pool) { + if (kex_pool == NULL) { kex_pool = make_sub_pool(sftp_pool); pr_pool_tag(kex_pool, "Kex Pool"); } @@ -5085,4 +5160,3 @@ int sftp_kex_send_first_kexinit(void) { destroy_pool(pkt->pool); return 0; } - diff --git a/contrib/mod_sftp/mod_sftp.c b/contrib/mod_sftp/mod_sftp.c index a88162c0d..1f94e4d9d 100644 --- a/contrib/mod_sftp/mod_sftp.c +++ b/contrib/mod_sftp/mod_sftp.c @@ -1,6 +1,6 @@ /* * ProFTPD - mod_sftp - * Copyright (c) 2008-2022 TJ Saunders + * Copyright (c) 2008-2023 TJ Saunders * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1415,8 +1415,9 @@ MODRET set_sftpoptions(cmd_rec *cmd) { config_rec *c; unsigned long opts = 0UL; - if (cmd->argc-1 == 0) + if (cmd->argc-1 == 0) { CONF_ERROR(cmd, "wrong number of parameters"); + } CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); @@ -1480,6 +1481,9 @@ MODRET set_sftpoptions(cmd_rec *cmd) { } else if (strcmp(cmd->argv[i], "NoExtensionNegotiation") == 0) { opts |= SFTP_OPT_NO_EXT_INFO; + } else if (strcmp(cmd->argv[i], "NoStrictKex") == 0) { + opts |= SFTP_OPT_NO_STRICT_KEX; + } else { CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown SFTPOption '", cmd->argv[i], "'", NULL)); diff --git a/contrib/mod_sftp/mod_sftp.h.in b/contrib/mod_sftp/mod_sftp.h.in index 9af590e8e..bde722f23 100644 --- a/contrib/mod_sftp/mod_sftp.h.in +++ b/contrib/mod_sftp/mod_sftp.h.in @@ -126,23 +126,24 @@ #define SFTP_SESS_STATE_HAVE_EXT_INFO 0x00010 /* mod_sftp option flags */ -#define SFTP_OPT_IGNORE_SFTP_UPLOAD_PERMS 0x00001 -#define SFTP_OPT_IGNORE_SCP_UPLOAD_PERMS 0x00002 -#define SFTP_OPT_PESSIMISTIC_KEXINIT 0x00004 -#define SFTP_OPT_OLD_PROTO_COMPAT 0x00008 -#define SFTP_OPT_MATCH_KEY_SUBJECT 0x00010 -#define SFTP_OPT_IGNORE_SFTP_SET_PERMS 0x00020 -#define SFTP_OPT_IGNORE_SFTP_SET_TIMES 0x00040 -#define SFTP_OPT_IGNORE_SFTP_SET_OWNERS 0x00080 -#define SFTP_OPT_IGNORE_SCP_UPLOAD_TIMES 0x00100 -#define SFTP_OPT_ALLOW_INSECURE_LOGIN 0x00200 -#define SFTP_OPT_INSECURE_HOSTKEY_PERMS 0x00400 -#define SFTP_OPT_ALLOW_WEAK_DH 0x00800 -#define SFTP_OPT_IGNORE_FIFOS 0x01000 -#define SFTP_OPT_IGNORE_SFTP_UPLOAD_XATTRS 0x02000 -#define SFTP_OPT_IGNORE_SFTP_SET_XATTRS 0x04000 -#define SFTP_OPT_INCLUDE_SFTP_TIMES 0x08000 -#define SFTP_OPT_NO_EXT_INFO 0x10000 +#define SFTP_OPT_IGNORE_SFTP_UPLOAD_PERMS 0x000001 +#define SFTP_OPT_IGNORE_SCP_UPLOAD_PERMS 0x000002 +#define SFTP_OPT_PESSIMISTIC_KEXINIT 0x000004 +#define SFTP_OPT_OLD_PROTO_COMPAT 0x000008 +#define SFTP_OPT_MATCH_KEY_SUBJECT 0x000010 +#define SFTP_OPT_IGNORE_SFTP_SET_PERMS 0x000020 +#define SFTP_OPT_IGNORE_SFTP_SET_TIMES 0x000040 +#define SFTP_OPT_IGNORE_SFTP_SET_OWNERS 0x000080 +#define SFTP_OPT_IGNORE_SCP_UPLOAD_TIMES 0x000100 +#define SFTP_OPT_ALLOW_INSECURE_LOGIN 0x000200 +#define SFTP_OPT_INSECURE_HOSTKEY_PERMS 0x000400 +#define SFTP_OPT_ALLOW_WEAK_DH 0x000800 +#define SFTP_OPT_IGNORE_FIFOS 0x001000 +#define SFTP_OPT_IGNORE_SFTP_UPLOAD_XATTRS 0x002000 +#define SFTP_OPT_IGNORE_SFTP_SET_XATTRS 0x004000 +#define SFTP_OPT_INCLUDE_SFTP_TIMES 0x008000 +#define SFTP_OPT_NO_EXT_INFO 0x010000 +#define SFTP_OPT_NO_STRICT_KEX 0x040000 /* mod_sftp service flags */ #define SFTP_SERVICE_FL_SFTP 0x0001 diff --git a/contrib/mod_sftp/packet.c b/contrib/mod_sftp/packet.c index b0067ea80..d8c97e92b 100644 --- a/contrib/mod_sftp/packet.c +++ b/contrib/mod_sftp/packet.c @@ -1742,6 +1742,18 @@ int sftp_ssh2_packet_rekey_set_size(off_t size) { return 0; } +uint32_t sftp_ssh2_packet_get_client_seqno(void) { + return packet_client_seqno; +} + +void sftp_ssh2_packet_reset_client_seqno(void) { + packet_client_seqno = 0; +} + +void sftp_ssh2_packet_reset_server_seqno(void) { + packet_server_seqno = 0; +} + int sftp_ssh2_packet_send_version(void) { if (!sent_version_id) { int res; diff --git a/contrib/mod_sftp/packet.h b/contrib/mod_sftp/packet.h index a424e9b25..fe538cbd7 100644 --- a/contrib/mod_sftp/packet.h +++ b/contrib/mod_sftp/packet.h @@ -1,6 +1,6 @@ /* * ProFTPD - mod_sftp packet IO - * Copyright (c) 2008-2020 TJ Saunders + * Copyright (c) 2008-2023 TJ Saunders * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -107,6 +107,13 @@ int sftp_ssh2_packet_rekey_reset(void); int sftp_ssh2_packet_rekey_set_seqno(uint32_t); int sftp_ssh2_packet_rekey_set_size(off_t); +/* These are used for implementing the "strict KEX" mitigations of the Terrapin + * attack (Issue 1760). + */ +uint32_t sftp_ssh2_packet_get_client_seqno(void); +void sftp_ssh2_packet_reset_client_seqno(void); +void sftp_ssh2_packet_reset_server_seqno(void); + int sftp_ssh2_packet_send_version(void); int sftp_ssh2_packet_set_poll_timeout(int); int sftp_ssh2_packet_set_version(const char *); diff --git a/contrib/mod_sftp/tap.c b/contrib/mod_sftp/tap.c index 95f388e43..7eaf959e2 100644 --- a/contrib/mod_sftp/tap.c +++ b/contrib/mod_sftp/tap.c @@ -1,6 +1,6 @@ /* * ProFTPD - mod_sftp traffic analysis protection - * Copyright (c) 2008-2016 TJ Saunders + * Copyright (c) 2008-2023 TJ Saunders * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -149,7 +149,6 @@ static void set_policy_chance(struct sftp_tap_policy *policy) { } static void set_policy_timer(struct sftp_tap_policy *policy) { - /* Start a timer which checks the last times we received and sent packets. * From there, we may want to inject a TAP message, depending on the * policy. @@ -177,6 +176,16 @@ int sftp_tap_send_packet(void) { int rnd; unsigned int chance; + /* Due to chances of violating client-side "strict KEX" Terrapin + * mitigations, we will not send packets if we are in the middle of a KEX. + */ + if (!(sftp_sess_state & SFTP_SESS_STATE_HAVE_KEX) || + (sftp_sess_state & SFTP_SESS_STATE_REKEYING)) { + pr_trace_msg(trace_channel, 11, + "unwilling to send TAP packet during KEX"); + return 0; + } + if (!sftp_interop_supports_feature(SFTP_SSH2_FEAT_IGNORE_MSG)) { pr_trace_msg(trace_channel, 3, "unable to send TAP packet: IGNORE not supported by client"); @@ -205,7 +214,7 @@ int sftp_tap_send_packet(void) { struct ssh2_packet *pkt; unsigned int max_datalen = 8192; - if (curr_policy.max_datalen) { + if (curr_policy.max_datalen > 0) { max_datalen = curr_policy.max_datalen; } @@ -246,15 +255,15 @@ int sftp_tap_send_packet(void) { int sftp_tap_set_policy(const char *policy) { register unsigned int i; - if (tap_pool) { + if (tap_pool != NULL) { /* Special case: IFF the existing policy is 'none' AND the given * policy is 'rogaway', just return. The 'none' policy must have been * explicitly configured, and it should override the automatic use of * the 'rogaway' policy. */ - if (strncmp(curr_policy.policy, "none", 5) == 0 && - strncasecmp(policy, "rogaway", 8) == 0) { + if (strcasecmp(curr_policy.policy, "none") == 0 && + strcasecmp(policy, "rogaway") == 0) { (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION, "'none' traffic policy explicitly configured, ignoring '%s' policy", policy); @@ -278,7 +287,6 @@ int sftp_tap_set_policy(const char *policy) { if (strcasecmp(tap_policies[i].policy, policy) == 0) { copy_policy(&curr_policy, &(tap_policies[i])); set_policy_chance(&curr_policy); - set_policy_timer(&curr_policy); return 0; } } @@ -286,3 +294,7 @@ int sftp_tap_set_policy(const char *policy) { errno = ENOENT; return -1; } + +void sftp_tap_start_policy(void) { + set_policy_timer(&curr_policy); +} diff --git a/contrib/mod_sftp/tap.h b/contrib/mod_sftp/tap.h index 4a4c065d2..312223595 100644 --- a/contrib/mod_sftp/tap.h +++ b/contrib/mod_sftp/tap.h @@ -1,6 +1,6 @@ /* * ProFTPD - mod_sftp traffic analysis protection - * Copyright (c) 2008-2016 TJ Saunders + * Copyright (c) 2008-2013 TJ Saunders * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -63,4 +63,7 @@ int sftp_tap_send_packet(void); */ int sftp_tap_set_policy(const char *); +/* Sets the configured TAP policy in motion. */ +void sftp_tap_start_policy(void); + #endif /* MOD_SFTP_TAP_H */ diff --git a/doc/contrib/mod_sftp.html b/doc/contrib/mod_sftp.html index 60c3436fa..cfc639d05 100644 --- a/doc/contrib/mod_sftp.html +++ b/doc/contrib/mod_sftp.html @@ -1186,6 +1186,19 @@ The currently implemented options are: proftpd-1.3.7rc4. +

+

  • NoStrictKex
    +

    + By default, mod_sftp will honor/support the OpenSSH + "strict KEX" mode extension, "kex-strict-c-v00@openssh.com" and + "kex-strict-s-v00@openssh.com". Use this option to disable support for + these custom OpenSSH extensions. + +

    + Note that this option first appeared in + proftpd-1.3.9rc1. +

  • +

  • OldProtocolCompat

    @@ -2642,7 +2655,7 @@ deal with this issue, then, you can hopefully upgrade to ProFTPD 1.3.6 or later,


    -© Copyright 2008-2021 TJ Saunders
    +© Copyright 2008-2023 TJ Saunders
    All Rights Reserved

    diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm index b4bdf516b..8c2be5465 100644 --- a/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm +++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm @@ -87,6 +87,11 @@ my $TESTS = { test_class => [qw(forking ssh2)], }, + ssh2_ext_kex_strict_terrapin_issue1760 => { + order => ++$order, + test_class => [qw(bug forking ssh2)], + }, + ssh2_hostkey_rsa => { order => ++$order, test_class => [qw(forking ssh2)], @@ -3885,6 +3890,218 @@ EOC unlink($log_file); } +sub ssh2_ext_kex_strict_terrapin_issue1760 { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'sftp'); + + my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key'); + my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key'); + + my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key'); + my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub'); + my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys'); + + my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys"); + unless (copy($rsa_rfc4716_key, $authorized_keys)) { + die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!"); + } + + my $ssh_config = File::Spec->rel2abs("$tmpdir/ssh.conf"); + if (open(my $fh, "> $ssh_config")) { + print $fh <rel2abs("$tmpdir/sftp-batch.conf"); + if (open(my $fh, "> $batch_file")) { + print $fh "ls -l\n"; + + unless (close($fh)) { + die("Can't write $batch_file: $!"); + } + + } else { + die("Can't open $batch_file: $!"); + } + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'ssh2:30 sftp:20 scp:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_sftp.c' => [ + "SFTPEngine on", + "SFTPLog $setup->{log_file}", + + "SFTPHostKey $rsa_host_key", + "SFTPHostKey $dsa_host_key", + + "SFTPAuthorizedUserKeys file:~/.authorized_keys", + ], + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + # Open pipes, for use between the parent and child processes. Specifically, + # the child will indicate when it's done with its test by writing a message + # to the parent. + my ($rfh, $wfh); + unless (pipe($rfh, $wfh)) { + die("Can't open pipe: $!"); + } + + require Net::SSH2; + + my $ex; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + # We use OpenSSH-9.6p1 to test our "strict KEX" Terrapin mitigations. + my $sftp = '/Users/tj/local/openssh-9.6p1/bin/sftp'; + + my @cmd = ( + $sftp, + '-F', + $ssh_config, + '-oBatchMode=yes', + '-oCheckHostIP=no', + '-oCompression=yes', + "-oPort=$port", + "-oIdentityFile=$rsa_priv_key", + '-oPubkeyAuthentication=yes', + '-oStrictHostKeyChecking=no', + '-oUserKnownHostsFile=/dev/null', + '-vvv', + '-b', + $batch_file, + "$setup->{user}\@127.0.0.1", + ); + + my $sftp_rh = IO::Handle->new(); + my $sftp_wh = IO::Handle->new(); + my $sftp_eh = IO::Handle->new(); + + $sftp_wh->autoflush(1); + + sleep(1); + + local $SIG{CHLD} = 'DEFAULT'; + + # Make sure that the perms on the priv key are what OpenSSH wants + unless (chmod(0400, $rsa_priv_key)) { + die("Can't set perms on $rsa_priv_key to 0400: $!"); + } + + if ($ENV{TEST_VERBOSE}) { + print STDERR "Executing: ", join(' ', @cmd), "\n"; + } + + my $sftp_pid = open3($sftp_wh, $sftp_rh, $sftp_eh, @cmd); + waitpid($sftp_pid, 0); + my $exit_status = $?; + + # Restore the perms on the priv key + unless (chmod(0644, $rsa_priv_key)) { + die("Can't set perms on $rsa_priv_key to 0644: $!"); + } + + my ($res, $errstr); + if ($exit_status >> 8 == 0) { + $errstr = join('', <$sftp_eh>); + $res = 0; + + } else { + $errstr = join('', <$sftp_eh>); + if ($ENV{TEST_VERBOSE}) { + print STDERR "Stderr: $errstr\n"; + } + + $res = 1; + } + + unless ($res == 0) { + die("Can't list files on server: $errstr"); + } + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + eval { + if (open(my $fh, "< $setup->{log_file}")) { + my $ok = 0; + + while (my $line = <$fh>) { + chomp($line); + + if ($ENV{TEST_VERBOSE}) { + print STDERR "# $line\n"; + } + + if ($line =~ /client signaled strict KEX support/) { + $ok = 1; + last; + } + } + + close($fh); + + $self->assert($ok, test_msg("Did not see expected 'strict KEX' TraceLog message")); + + } else { + die("Can't read $setup->{log_file}: $!"); + } + }; + if ($@) { + $ex = $@; + } + + test_cleanup($setup->{log_file}, $ex); +} + sub ssh2_hostkey_rsa { my $self = shift; my $tmpdir = $self->{tmpdir};