| From 3766203c9081eec9c932cf16c9e34b00d14c11ef Mon Sep 17 00:00:00 2001 |
| From: Marc Deslauriers <marc.deslauriers@ubuntu.com> |
| Date: Tue, 2 Jan 2024 11:54:04 -0500 |
| Subject: [PATCH] upstream: implement "strict key exchange" in ssh and sshd |
| |
| Gbp-Pq: CVE-2023-48795.patch. |
| --- |
| PROTOCOL | 26 +++++++++++++++++ |
| kex.c | 68 +++++++++++++++++++++++++++++++++----------- |
| kex.h | 1 + |
| packet.c | 78 ++++++++++++++++++++++++++++++++++++++------------- |
| sshconnect2.c | 14 +++------ |
| sshd.c | 7 +++-- |
| 6 files changed, 146 insertions(+), 48 deletions(-) |
| |
| diff --git a/PROTOCOL b/PROTOCOL |
| index 0b2ea60a7..d126409c4 100644 |
| --- a/PROTOCOL |
| +++ b/PROTOCOL |
| @@ -102,6 +102,32 @@ OpenSSH supports the use of ECDH in Curve25519 for key exchange as |
| described at: |
| http://git.libssh.org/users/aris/libssh.git/plain/doc/curve25519-sha256@libssh.org.txt?h=curve25519 |
| |
| +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 |
| +RFC8308 ext-info feature: by including a additional algorithm in the |
| +initiial 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". These pseudo-algorithms |
| +are only valid in the initial SSH2_MSG_KEXINIT and MUST be ignored |
| +if they are present in subsequent SSH2_MSG_KEXINIT packets. |
| + |
| +When an 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. |
| + Unexpected packets for the purpose of strict KEX include messages |
| + that are otherwise valid at any time during the connection such as |
| + SSH2_MSG_DEBUG and SSH2_MSG_IGNORE. |
| +b) After sending or receiving a SSH2_MSG_NEWKEYS message, reset the |
| + packet sequence number to zero. This behaviour persists for the |
| + duration of the connection (i.e. not just the first |
| + SSH2_MSG_NEWKEYS). |
| + |
| 2. Connection protocol changes |
| |
| 2.1. connection: Channel write close extension "eow@openssh.com" |
| diff --git a/kex.c b/kex.c |
| index 30425ab8a..40159e045 100644 |
| --- a/kex.c |
| +++ b/kex.c |
| @@ -63,7 +63,7 @@ |
| #include "digest.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 *proposal_names[PROPOSAL_MAX] = { |
| @@ -175,6 +175,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. |
| @@ -182,7 +194,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') |
| @@ -199,10 +211,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); |
| @@ -410,7 +420,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) |
| @@ -482,6 +497,11 @@ kex_input_ext_info(int type, u_int32_t seq, struct ssh *ssh) |
| ssh_dispatch_set(ssh, SSH2_MSG_EXT_INFO, &kex_protocol_error); |
| if ((r = sshpkt_get_u32(ssh, &ninfo)) != 0) |
| return r; |
| + if (ninfo >= 1024) { |
| + error("SSH2_MSG_EXT_INFO with too many entries, expected " |
| + "<=1024, received %u", ninfo); |
| + return dispatch_protocol_error(type, seq, ssh); |
| + } |
| for (i = 0; i < ninfo; i++) { |
| if ((r = sshpkt_get_cstring(ssh, &name, NULL)) != 0) |
| return r; |
| @@ -582,7 +602,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 ((r = sshbuf_put(kex->peer, ptr, dlen)) != 0) |
| return r; |
| @@ -618,7 +638,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) |
| @@ -881,7 +901,13 @@ proposals_match(char *my[PROPOSAL_MAX], char *peer[PROPOSAL_MAX]) |
| } |
| |
| static int |
| -kex_choose_conf(struct ssh *ssh) |
| +kexalgs_contains(char **peer, const char *ext) |
| +{ |
| + return has_any_alg(peer[PROPOSAL_KEX_ALGS], ext); |
| +} |
| + |
| +static int |
| +kex_choose_conf(struct ssh *ssh, uint32_t seq) |
| { |
| struct kex *kex = ssh->kex; |
| struct newkeys *newkeys; |
| @@ -906,13 +932,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"); |
| + } |
| } |
| |
| /* Algorithm Negotiation */ |
| diff --git a/kex.h b/kex.h |
| index 9605ed528..f8bbd88ac 100644 |
| --- a/kex.h |
| +++ b/kex.h |
| @@ -143,6 +143,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 4bd8b4ec9..ab3001767 100644 |
| --- a/packet.c |
| +++ b/packet.c |
| @@ -1196,8 +1196,13 @@ ssh_packet_send2_wrapped(struct ssh *ssh) |
| sshbuf_dump(state->output, stderr); |
| #endif |
| /* increment sequence number for outgoing packets */ |
| - if (++state->p_send.seqnr == 0) |
| + if (++state->p_send.seqnr == 0) { |
| + if ((ssh->kex->flags & KEX_INITIAL) != 0) { |
| + ssh_packet_disconnect(ssh, "outgoing sequence number " |
| + "wrapped during initial key exchange"); |
| + } |
| logit("outgoing seqnr wraps around"); |
| + } |
| if (++state->p_send.packets == 0) |
| if (!(ssh->compat & SSH_BUG_NOREKEY)) |
| return SSH_ERR_NEED_REKEY; |
| @@ -1205,6 +1210,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) |
| @@ -1337,8 +1347,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) |
| @@ -1625,10 +1634,16 @@ 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) |
| + if (++state->p_read.seqnr == 0) { |
| + if ((ssh->kex->flags & KEX_INITIAL) != 0) { |
| + ssh_packet_disconnect(ssh, "incoming sequence number " |
| + "wrapped during initial key exchange"); |
| + } |
| logit("incoming seqnr wraps around"); |
| + } |
| if (++state->p_read.packets == 0) |
| if (!(ssh->compat & SSH_BUG_NOREKEY)) |
| return SSH_ERR_NEED_REKEY; |
| @@ -1694,6 +1709,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; |
| + } |
| |
| /* do we need to rekey? */ |
| if (ssh_packet_need_rekeying(ssh, 0)) { |
| @@ -1718,10 +1737,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"); |
| @@ -1736,19 +1784,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; |
| @@ -2201,6 +2236,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 || |
| @@ -2363,6 +2399,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 || |
| @@ -2691,6 +2728,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/sshconnect2.c b/sshconnect2.c |
| index 059c9480d..8e8de2af9 100644 |
| --- a/sshconnect2.c |
| +++ b/sshconnect2.c |
| @@ -241,7 +241,8 @@ ssh_kex2(struct ssh *ssh, char *host, struct sockaddr *hostaddr, u_short port, |
| fatal_fr(r, "kex_assemble_namelist"); |
| free(all_key); |
| |
| - if ((s = kex_names_cat(options.kex_algorithms, "ext-info-c")) == NULL) |
| + if ((s = kex_names_cat(options.kex_algorithms, |
| + "ext-info-c,kex-strict-c-v00@openssh.com")) == NULL) |
| fatal_f("kex_names_cat"); |
| myproposal[PROPOSAL_KEX_ALGS] = compat_kex_proposal(ssh, s); |
| myproposal[PROPOSAL_ENC_ALGS_CTOS] = |
| @@ -363,7 +364,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 *); |
| @@ -479,7 +479,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); |
| @@ -524,13 +524,6 @@ input_userauth_service_accept(int type, u_int32_t seq, struct ssh *ssh) |
| return r; |
| } |
| |
| -/* ARGSUSED */ |
| -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) |
| { |
| @@ -612,6 +605,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; |
| } |
| |
| diff --git a/sshd.c b/sshd.c |
| index 6277e6d6d..953370311 100644 |
| --- a/sshd.c |
| +++ b/sshd.c |
| @@ -2340,11 +2340,13 @@ static void |
| do_ssh2_kex(struct ssh *ssh) |
| { |
| char *myproposal[PROPOSAL_MAX] = { KEX_SERVER }; |
| + char *s; |
| struct kex *kex; |
| int r; |
| |
| - myproposal[PROPOSAL_KEX_ALGS] = compat_kex_proposal(ssh, |
| - options.kex_algorithms); |
| + if ((s = kex_names_cat(options.kex_algorithms, "kex-strict-s-v00@openssh.com")) == NULL) |
| + fatal_f("kex_names_cat"); |
| + myproposal[PROPOSAL_KEX_ALGS] = compat_kex_proposal(ssh, s); |
| myproposal[PROPOSAL_ENC_ALGS_CTOS] = compat_cipher_proposal(ssh, |
| options.ciphers); |
| myproposal[PROPOSAL_ENC_ALGS_STOC] = compat_cipher_proposal(ssh, |
| @@ -2397,6 +2399,7 @@ do_ssh2_kex(struct ssh *ssh) |
| (r = ssh_packet_write_wait(ssh)) != 0) |
| fatal_fr(r, "send test"); |
| #endif |
| + free(s); |
| debug("KEX done"); |
| } |
| |
| -- |
| 2.43.0.472.g3155946c3a-goog |