╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │985 host_key: *const HostKey, │ │986 client_version: []const u8, │ │987 ) !KexResult { │ │980 - var client_seq: u64 = 0; │ │981 - const transport = KexTransport{ .plain = &client_seq }; │ │988 + var seqs: PlainSeqs = .{}; │ │989 + const transport = KexTransport{ .plain = &seqs }; │ │990 │ │991 // exchange KEXINITs │ │992 const server_kex_init = try buildServerKexInit(io, allocator);│ ╰───────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │993 defer allocator.free(server_kex_init); │ │986 - try writePlainPacket(io, writer, server_kex_init); │ │994 + try transport.writePacket(io, writer, server_kex_init); │ │995 │ │996 const client_kex_init = try transport.readPacket(allocator, reader, false);│ │997 defer allocator.free(client_kex_init); │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │ 998 + │ │ 999 + var parsed = try parseClientKexInit(allocator, client_kex_init); │ │1000 + defer parsed.deinit(allocator); │ │1001 │ │1002 // strict kex: when the client advertises it, its KEXINIT must be the │ │1003 // first packet, ignorable messages are banned until NEWKEYS, and both│ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │1004 // seqnos reset to 0 after every NEWKEYS. │ │ 994 - const strict = strict: { │ │ 995 - var parsed = try parseClientKexInit(allocator, client_kex_init); │ │ 996 - defer parsed.deinit(allocator); │ │ 997 - break :strict nameListContainsAny(parsed.kex_algos, &.{kex_strict_client});│ │ 998 - }; │ │ 999 - if (strict and client_seq != 1) return error.StrictKexViolation; │ │1005 + const strict = nameListContainsAny(parsed.kex_algos, &.{kex_strict_client}); │ │1006 + if (strict and seqs.read != 1) return error.StrictKexViolation; │ │1007 │ │1008 const negotiated = try exchangeKeys( │ │1009 io, │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────╮ │1016 client_version, │ │1017 client_kex_init,│ │1018 server_kex_init,│ │1019 + &parsed, │ │1020 null, │ │1021 ); │ │1022 │ ╰──────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │1024 .cs_key = negotiated.keys.cs_enc, │ │1025 .sc_key = negotiated.keys.sc_enc, │ │1026 .session_id = negotiated.exchange_hash, │ │1019 - // the server sent exactly KEXINIT, KEX_ECDH_REPLY and NEWKEYS; the│ │1020 - // client's count includes any skipped IGNORE/DEBUG packets. │ │1021 - .next_cs_seq = if (strict) 0 else client_seq, │ │1022 - .next_sc_seq = if (strict) 0 else 3, │ │1027 + // each cipher continues its own direction's count; the read side │ │1028 + // includes any skipped IGNORE/DEBUG packets. │ │1029 + .next_cs_seq = if (strict) 0 else seqs.read, │ │1030 + .next_sc_seq = if (strict) 0 else seqs.written, │ │1031 .strict = strict, │ │1032 }; │ │1033 } │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │1044 // the whole exchange under the current keys, then swap the new keys into │ │1045 // both ciphers in place. │ │1046 fn runRekey(conn: *Conn, client_kex_init: []const u8) !void { │ │1047 + var parsed = try parseClientKexInit(conn.allocator, client_kex_init); │ │1048 + defer parsed.deinit(conn.allocator); │ │1049 + │ │1050 const server_kex_init = try buildServerKexInit(conn.io, conn.allocator);│ │1051 defer conn.allocator.free(server_kex_init); │ │1052 try conn.sc_cipher.writePacket(conn.io, conn.writer, server_kex_init); │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────╮ │1062 conn.rekey.client_version,│ │1063 client_kex_init, │ │1064 server_kex_init, │ │1065 + &parsed, │ │1066 &conn.rekey.session_id, │ │1067 ); │ │1068 │ ╰────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │1079 │ │1080 // the KEX core shared by the initial exchange and rekeys: pick algorithms, │ │1081 // run the (hybrid) ECDH round trip, exchange NEWKEYS, derive keys. both │ │1070 -// KEXINIT payloads have already been exchanged by the caller. session_id is │ │1071 -// null on the initial exchange (where it becomes the exchange hash) and the │ │1072 -// original session id on a rekey. │ │1082 +// KEXINIT payloads have already been exchanged by the caller, which also owns │ │1083 +// `parsed`; the client's raw payload comes along too because the exchange hash│ │1084 +// covers it verbatim. session_id is null on the initial exchange (where it │ │1085 +// becomes the exchange hash) and the original session id on a rekey. │ │1086 fn exchangeKeys( │ │1087 io: std.Io, │ │1088 allocator: std.mem.Allocator, │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │1094 client_version: []const u8, │ │1095 client_kex_init: []const u8, │ │1096 server_kex_init: []const u8, │ │1097 + parsed: *const ParsedKexInit, │ │1098 session_id: ?*const [Sha256.digest_length]u8, │ │1099 ) !NegotiatedKeys { │ │1086 - var parsed = try parseClientKexInit(allocator, client_kex_init); │ │1087 - defer parsed.deinit(allocator); │ │1088 - │ │1100 // pick the first kex algo in the client's preference list that we also│ │1101 // support (RFC 4253 §7.1.3). │ │1102 const kex_algo = blk: { │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯