diff --git a/PROTOCOL b/PROTOCOL index d453c779b..ca6be6f6e 100644 --- a/PROTOCOL +++ b/PROTOCOL @@ -137,6 +137,25 @@ than as a named global or channel request to allow pings with very short packet lengths, which would not be possible with other approaches. +1.9 transport: strict key exchange extension + +OpenSSH supports a number of transport-layer hardening measures under +a "strict KEX" feature. This feature is signalled similarly to the +RFC8305 ext-info feature: by including a additional algorithm in the +SSH2_MSG_KEXINIT kex_algorithms field. The client may append +"kex-strict-c-v00@openssh.com" to its kex_algorithms and the server +may append "kex-strict-s-v00@openssh.com". + +When endpoint that supports this extension observes this algorithm +name in a peer's KEXINIT packet, it MUST make the following changes to +the the protocol: + +a) During initial KEX, terminate the connection if any unexpected or + out-of-sequence packet is received. This includes terminating the + connection if the first packet received is not SSH2_MSG_KEXINIT. +b) At each SSH2_MSG_NEWKEYS message, reset the packet sequence number + to zero. + 2. Connection protocol changes 2.1. connection: Channel write close extension "eow@openssh.com" diff --git a/kex.c b/kex.c index f88965aee..445371857 100644 --- a/kex.c +++ b/kex.c @@ -69,7 +69,7 @@ #include "xmalloc.h" /* prototype */ -static int kex_choose_conf(struct ssh *); +static int kex_choose_conf(struct ssh *, uint32_t seq); static int kex_input_newkeys(int, u_int32_t, struct ssh *); static const char * const proposal_names[PROPOSAL_MAX] = { @@ -181,6 +181,18 @@ kex_names_valid(const char *names) return 1; } +/* returns non-zero if proposal contains any algorithm from algs */ +static int +has_any_alg(const char *proposal, const char *algs) +{ + char *cp; + + if ((cp = match_list(proposal, algs, NULL)) == NULL) + return 0; + free(cp); + return 1; +} + /* * Concatenate algorithm names, avoiding duplicates in the process. * Caller must free returned string. @@ -188,7 +200,7 @@ kex_names_valid(const char *names) char * kex_names_cat(const char *a, const char *b) { - char *ret = NULL, *tmp = NULL, *cp, *p, *m; + char *ret = NULL, *tmp = NULL, *cp, *p; size_t len; if (a == NULL || *a == '\0') @@ -205,10 +217,8 @@ kex_names_cat(const char *a, const char *b) } strlcpy(ret, a, len); for ((p = strsep(&cp, ",")); p && *p != '\0'; (p = strsep(&cp, ","))) { - if ((m = match_list(ret, p, NULL)) != NULL) { - free(m); + if (has_any_alg(ret, p)) continue; /* Algorithm already present */ - } if (strlcat(ret, ",", len) >= len || strlcat(ret, p, len) >= len) { free(tmp); @@ -338,15 +348,23 @@ kex_proposal_populate_entries(struct ssh *ssh, char *prop[PROPOSAL_MAX], const char *defpropclient[PROPOSAL_MAX] = { KEX_CLIENT }; const char **defprop = ssh->kex->server ? defpropserver : defpropclient; u_int i; + char *cp; if (prop == NULL) fatal_f("proposal missing"); + /* Append EXT_INFO signalling to KexAlgorithms */ + if (kexalgos == NULL) + kexalgos = defprop[PROPOSAL_KEX_ALGS]; + if ((cp = kex_names_cat(kexalgos, ssh->kex->server ? + "kex-strict-s-v00@openssh.com" : + "ext-info-c,kex-strict-c-v00@openssh.com")) == NULL) + fatal_f("kex_names_cat"); + for (i = 0; i < PROPOSAL_MAX; i++) { switch(i) { case PROPOSAL_KEX_ALGS: - prop[i] = compat_kex_proposal(ssh, - kexalgos ? kexalgos : defprop[i]); + prop[i] = compat_kex_proposal(ssh, cp); break; case PROPOSAL_ENC_ALGS_CTOS: case PROPOSAL_ENC_ALGS_STOC: @@ -367,6 +385,7 @@ kex_proposal_populate_entries(struct ssh *ssh, char *prop[PROPOSAL_MAX], prop[i] = xstrdup(defprop[i]); } } + free(cp); } void @@ -470,7 +489,12 @@ kex_protocol_error(int type, u_int32_t seq, struct ssh *ssh) { int r; - error("kex protocol error: type %d seq %u", type, seq); + /* If in strict mode, any unexpected message is an error */ + if ((ssh->kex->flags & KEX_INITIAL) && ssh->kex->kex_strict) { + ssh_packet_disconnect(ssh, "strict KEX violation: " + "unexpected packet type %u (seqnr %u)", type, seq); + } + error_f("type %u seq %u", type, seq); if ((r = sshpkt_start(ssh, SSH2_MSG_UNIMPLEMENTED)) != 0 || (r = sshpkt_put_u32(ssh, seq)) != 0 || (r = sshpkt_send(ssh)) != 0) @@ -567,7 +591,7 @@ kex_input_ext_info(int type, u_int32_t seq, struct ssh *ssh) if (ninfo >= 1024) { error("SSH2_MSG_EXT_INFO with too many entries, expected " "<=1024, received %u", ninfo); - return SSH_ERR_INVALID_FORMAT; + return dispatch_protocol_error(type, seq, ssh); } for (i = 0; i < ninfo; i++) { if ((r = sshpkt_get_cstring(ssh, &name, NULL)) != 0) @@ -685,7 +709,7 @@ kex_input_kexinit(int type, u_int32_t seq, struct ssh *ssh) error_f("no kex"); return SSH_ERR_INTERNAL_ERROR; } - ssh_dispatch_set(ssh, SSH2_MSG_KEXINIT, NULL); + ssh_dispatch_set(ssh, SSH2_MSG_KEXINIT, &kex_protocol_error); ptr = sshpkt_ptr(ssh, &dlen); if (ptr == NULL) { // fix CodeQL SM02313 error_f("kex packet pointer failure"); @@ -725,7 +749,7 @@ kex_input_kexinit(int type, u_int32_t seq, struct ssh *ssh) if (!(kex->flags & KEX_INIT_SENT)) if ((r = kex_send_kexinit(ssh)) != 0) return r; - if ((r = kex_choose_conf(ssh)) != 0) + if ((r = kex_choose_conf(ssh, seq)) != 0) return r; if (kex->kex_type < KEX_MAX && kex->kex[kex->kex_type] != NULL) @@ -989,20 +1013,14 @@ proposals_match(char *my[PROPOSAL_MAX], char *peer[PROPOSAL_MAX]) return (1); } -/* returns non-zero if proposal contains any algorithm from algs */ static int -has_any_alg(const char *proposal, const char *algs) +kexalgs_contains(char **peer, const char *ext) { - char *cp; - - if ((cp = match_list(proposal, algs, NULL)) == NULL) - return 0; - free(cp); - return 1; + return has_any_alg(peer[PROPOSAL_KEX_ALGS], ext); } static int -kex_choose_conf(struct ssh *ssh) +kex_choose_conf(struct ssh *ssh, uint32_t seq) { struct kex *kex = ssh->kex; struct newkeys *newkeys; @@ -1027,13 +1045,23 @@ kex_choose_conf(struct ssh *ssh) sprop=peer; } - /* Check whether client supports ext_info_c */ - if (kex->server && (kex->flags & KEX_INITIAL)) { - char *ext; - - ext = match_list("ext-info-c", peer[PROPOSAL_KEX_ALGS], NULL); - kex->ext_info_c = (ext != NULL); - free(ext); + /* Check whether peer supports ext_info/kex_strict */ + if ((kex->flags & KEX_INITIAL) != 0) { + if (kex->server) { + kex->ext_info_c = kexalgs_contains(peer, "ext-info-c"); + kex->kex_strict = kexalgs_contains(peer, + "kex-strict-c-v00@openssh.com"); + } else { + kex->kex_strict = kexalgs_contains(peer, + "kex-strict-s-v00@openssh.com"); + } + if (kex->kex_strict) { + debug3_f("will use strict KEX ordering"); + if (seq != 0) + ssh_packet_disconnect(ssh, + "strict KEX violation: " + "KEXINIT was not the first packet"); + } } /* Check whether client supports rsa-sha2 algorithms */ diff --git a/kex.h b/kex.h index 5f7ef784e..e9fece08f 100644 --- a/kex.h +++ b/kex.h @@ -149,6 +149,7 @@ struct kex { u_int kex_type; char *server_sig_algs; int ext_info_c; + int kex_strict; struct sshbuf *my; struct sshbuf *peer; struct sshbuf *client_version; diff --git a/packet.c b/packet.c index 97c2011ec..6d6a53779 100644 --- a/packet.c +++ b/packet.c @@ -1216,6 +1216,11 @@ ssh_packet_send2_wrapped(struct ssh *ssh) state->p_send.bytes += len; sshbuf_reset(state->outgoing_packet); + if (type == SSH2_MSG_NEWKEYS && ssh->kex->kex_strict) { + debug_f("resetting send seqnr %u", state->p_send.seqnr); + state->p_send.seqnr = 0; + } + if (type == SSH2_MSG_NEWKEYS) r = ssh_set_newkeys(ssh, MODE_OUT); else if (type == SSH2_MSG_USERAUTH_SUCCESS && state->server_side) @@ -1344,8 +1349,7 @@ ssh_packet_read_seqnr(struct ssh *ssh, u_char *typep, u_int32_t *seqnr_p) /* Stay in the loop until we have received a complete packet. */ for (;;) { /* Try to read a packet from the buffer. */ - r = ssh_packet_read_poll_seqnr(ssh, typep, seqnr_p); - if (r != 0) + if ((r = ssh_packet_read_poll_seqnr(ssh, typep, seqnr_p)) != 0) break; /* If we got a packet, return it. */ if (*typep != SSH_MSG_NONE) @@ -1416,29 +1420,6 @@ ssh_packet_read(struct ssh *ssh) return type; } -/* - * Waits until a packet has been received, verifies that its type matches - * that given, and gives a fatal error and exits if there is a mismatch. - */ - -int -ssh_packet_read_expect(struct ssh *ssh, u_int expected_type) -{ - int r; - u_char type; - - if ((r = ssh_packet_read_seqnr(ssh, &type, NULL)) != 0) - return r; - if (type != expected_type) { - if ((r = sshpkt_disconnect(ssh, - "Protocol error: expected packet type %d, got %d", - expected_type, type)) != 0) - return r; - return SSH_ERR_PROTOCOL_ERROR; - } - return 0; -} - static int ssh_packet_read_poll2_mux(struct ssh *ssh, u_char *typep, u_int32_t *seqnr_p) { @@ -1632,6 +1613,7 @@ ssh_packet_read_poll2(struct ssh *ssh, u_char *typep, u_int32_t *seqnr_p) if ((r = sshbuf_consume(state->input, mac->mac_len)) != 0) goto out; } + if (seqnr_p != NULL) *seqnr_p = state->p_read.seqnr; if (++state->p_read.seqnr == 0) @@ -1701,6 +1683,10 @@ ssh_packet_read_poll2(struct ssh *ssh, u_char *typep, u_int32_t *seqnr_p) #endif /* reset for next packet */ state->packlen = 0; + if (*typep == SSH2_MSG_NEWKEYS && ssh->kex->kex_strict) { + debug_f("resetting read seqnr %u", state->p_read.seqnr); + state->p_read.seqnr = 0; + } if ((r = ssh_packet_check_rekey(ssh)) != 0) return r; @@ -1723,10 +1709,39 @@ ssh_packet_read_poll_seqnr(struct ssh *ssh, u_char *typep, u_int32_t *seqnr_p) r = ssh_packet_read_poll2(ssh, typep, seqnr_p); if (r != 0) return r; - if (*typep) { - state->keep_alive_timeouts = 0; - DBG(debug("received packet type %d", *typep)); + if (*typep == 0) { + /* no message ready */ + return 0; } + state->keep_alive_timeouts = 0; + DBG(debug("received packet type %d", *typep)); + + /* Always process disconnect messages */ + if (*typep == SSH2_MSG_DISCONNECT) { + if ((r = sshpkt_get_u32(ssh, &reason)) != 0 || + (r = sshpkt_get_string(ssh, &msg, NULL)) != 0) + return r; + /* Ignore normal client exit notifications */ + do_log2(ssh->state->server_side && + reason == SSH2_DISCONNECT_BY_APPLICATION ? + SYSLOG_LEVEL_INFO : SYSLOG_LEVEL_ERROR, + "Received disconnect from %s port %d:" + "%u: %.400s", ssh_remote_ipaddr(ssh), + ssh_remote_port(ssh), reason, msg); + free(msg); + return SSH_ERR_DISCONNECTED; + } + + /* + * Do not implicitly handle any messages here during initial + * KEX when in strict mode. They will be need to be allowed + * explicitly by the KEX dispatch table or they will generate + * protocol errors. + */ + if (ssh->kex != NULL && + (ssh->kex->flags & KEX_INITIAL) && ssh->kex->kex_strict) + return 0; + /* Implicitly handle transport-level messages */ switch (*typep) { case SSH2_MSG_IGNORE: debug3("Received SSH2_MSG_IGNORE"); @@ -1741,19 +1756,6 @@ ssh_packet_read_poll_seqnr(struct ssh *ssh, u_char *typep, u_int32_t *seqnr_p) debug("Remote: %.900s", msg); free(msg); break; - case SSH2_MSG_DISCONNECT: - if ((r = sshpkt_get_u32(ssh, &reason)) != 0 || - (r = sshpkt_get_string(ssh, &msg, NULL)) != 0) - return r; - /* Ignore normal client exit notifications */ - do_log2(ssh->state->server_side && - reason == SSH2_DISCONNECT_BY_APPLICATION ? - SYSLOG_LEVEL_INFO : SYSLOG_LEVEL_ERROR, - "Received disconnect from %s port %d:" - "%u: %.400s", ssh_remote_ipaddr(ssh), - ssh_remote_port(ssh), reason, msg); - free(msg); - return SSH_ERR_DISCONNECTED; case SSH2_MSG_UNIMPLEMENTED: if ((r = sshpkt_get_u32(ssh, &seqnr)) != 0) return r; @@ -2245,6 +2247,7 @@ kex_to_blob(struct sshbuf *m, struct kex *kex) (r = sshbuf_put_u32(m, kex->hostkey_type)) != 0 || (r = sshbuf_put_u32(m, kex->hostkey_nid)) != 0 || (r = sshbuf_put_u32(m, kex->kex_type)) != 0 || + (r = sshbuf_put_u32(m, kex->kex_strict)) != 0 || (r = sshbuf_put_stringb(m, kex->my)) != 0 || (r = sshbuf_put_stringb(m, kex->peer)) != 0 || (r = sshbuf_put_stringb(m, kex->client_version)) != 0 || @@ -2407,6 +2410,7 @@ kex_from_blob(struct sshbuf *m, struct kex **kexp) (r = sshbuf_get_u32(m, (u_int *)&kex->hostkey_type)) != 0 || (r = sshbuf_get_u32(m, (u_int *)&kex->hostkey_nid)) != 0 || (r = sshbuf_get_u32(m, &kex->kex_type)) != 0 || + (r = sshbuf_get_u32(m, &kex->kex_strict)) != 0 || (r = sshbuf_get_stringb(m, kex->my)) != 0 || (r = sshbuf_get_stringb(m, kex->peer)) != 0 || (r = sshbuf_get_stringb(m, kex->client_version)) != 0 || @@ -2737,6 +2741,7 @@ sshpkt_disconnect(struct ssh *ssh, const char *fmt,...) vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); + debug2_f("sending SSH2_MSG_DISCONNECT: %s", buf); if ((r = sshpkt_start(ssh, SSH2_MSG_DISCONNECT)) != 0 || (r = sshpkt_put_u32(ssh, SSH2_DISCONNECT_PROTOCOL_ERROR)) != 0 || (r = sshpkt_put_cstring(ssh, buf)) != 0 || diff --git a/packet.h b/packet.h index 11925a27d..660fd4d5b 100644 --- a/packet.h +++ b/packet.h @@ -124,7 +124,6 @@ int ssh_packet_send2_wrapped(struct ssh *); int ssh_packet_send2(struct ssh *); int ssh_packet_read(struct ssh *); -int ssh_packet_read_expect(struct ssh *, u_int type); int ssh_packet_read_poll(struct ssh *); int ssh_packet_read_poll2(struct ssh *, u_char *, u_int32_t *seqnr_p); int ssh_packet_process_incoming(struct ssh *, const char *buf, u_int len); diff --git a/sshconnect2.c b/sshconnect2.c index c8c886c85..bc0904442 100644 --- a/sshconnect2.c +++ b/sshconnect2.c @@ -362,7 +362,6 @@ struct cauthmethod { }; static int input_userauth_service_accept(int, u_int32_t, struct ssh *); -static int input_userauth_ext_info(int, u_int32_t, struct ssh *); static int input_userauth_success(int, u_int32_t, struct ssh *); static int input_userauth_failure(int, u_int32_t, struct ssh *); static int input_userauth_banner(int, u_int32_t, struct ssh *); @@ -476,7 +475,7 @@ ssh_userauth2(struct ssh *ssh, const char *local_user, ssh->authctxt = &authctxt; ssh_dispatch_init(ssh, &input_userauth_error); - ssh_dispatch_set(ssh, SSH2_MSG_EXT_INFO, &input_userauth_ext_info); + ssh_dispatch_set(ssh, SSH2_MSG_EXT_INFO, kex_input_ext_info); ssh_dispatch_set(ssh, SSH2_MSG_SERVICE_ACCEPT, &input_userauth_service_accept); ssh_dispatch_run_fatal(ssh, DISPATCH_BLOCK, &authctxt.success); /* loop until success */ pubkey_cleanup(ssh); @@ -530,12 +529,6 @@ input_userauth_service_accept(int type, u_int32_t seq, struct ssh *ssh) return r; } -static int -input_userauth_ext_info(int type, u_int32_t seqnr, struct ssh *ssh) -{ - return kex_input_ext_info(type, seqnr, ssh); -} - void userauth(struct ssh *ssh, char *authlist) { @@ -614,6 +607,7 @@ input_userauth_success(int type, u_int32_t seq, struct ssh *ssh) free(authctxt->methoddata); authctxt->methoddata = NULL; authctxt->success = 1; /* break out */ + ssh_dispatch_set(ssh, SSH2_MSG_EXT_INFO, dispatch_protocol_error); return 0; }