╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │910 client_version: []const u8, │ │911 ) !KexResult { │ │912 var client_seq: u64 = 0; │ │913 + const transport = KexTransport{ .plain = &client_seq }; │ │914 │ │915 // exchange KEXINITs │ │916 const server_kex_init = try buildServerKexInit(io, allocator);│ ╰───────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │ 917 defer allocator.free(server_kex_init); │ │ 918 try writePlainPacket(io, writer, server_kex_init); │ │ 919 │ │ 865 - const client_kex_init = try readPlainKexPacket(allocator, reader, &client_seq); │ │ 920 + const client_kex_init = try transport.readPacket(allocator, reader, false); │ │ 921 defer allocator.free(client_kex_init); │ │ 922 + │ │ 923 + // strict kex: when the client advertises it, its KEXINIT must be the │ │ 924 + // first packet, ignorable messages are banned until NEWKEYS, and both │ │ 925 + // seqnos reset to 0 after every NEWKEYS. │ │ 926 + const strict = strict: { │ │ 927 + var parsed = try parseClientKexInit(allocator, client_kex_init); │ │ 928 + defer parsed.deinit(allocator); │ │ 929 + break :strict nameListContainsAny(parsed.kex_algos, &.{kex_strict_client}); │ │ 930 + }; │ │ 931 + if (strict and client_seq != 1) return error.StrictKexViolation; │ │ 932 + │ │ 933 + const negotiated = try exchangeKeys( │ │ 934 + io, │ │ 935 + allocator, │ │ 936 + reader, │ │ 937 + writer, │ │ 938 + transport, │ │ 939 + strict, │ │ 940 + host_key, │ │ 941 + client_version, │ │ 942 + client_kex_init, │ │ 943 + server_kex_init, │ │ 944 + null, │ │ 945 + ); │ │ 946 + │ │ 947 + return .{ │ │ 948 + .cs_key = negotiated.keys.cs_enc, │ │ 949 + .sc_key = negotiated.keys.sc_enc, │ │ 950 + .session_id = negotiated.exchange_hash, │ │ 951 + // the server sent exactly KEXINIT, KEX_ECDH_REPLY and NEWKEYS; the │ │ 952 + // client's count includes any skipped IGNORE/DEBUG packets. │ │ 953 + .next_cs_seq = if (strict) 0 else client_seq, │ │ 954 + .next_sc_seq = if (strict) 0 else 3, │ │ 955 + .strict = strict, │ │ 956 + }; │ │ 957 +} │ │ 958 + │ │ 959 +/// everything needed to service a client-initiated rekey mid-session. │ │ 960 +pub const RekeyState = struct { │ │ 961 + host_key: *const HostKey, │ │ 962 + client_version: []const u8, │ │ 963 + session_id: [Sha256.digest_length]u8, │ │ 964 + strict: bool, │ │ 965 +}; │ │ 966 + │ │ 967 +// service a rekey whose triggering KEXINIT payload has just been read: run │ │ 968 +// the whole exchange under the current keys, then swap the new keys into │ │ 969 +// both ciphers in place. │ │ 970 +fn runRekey( │ │ 971 + io: std.Io, │ │ 972 + allocator: std.mem.Allocator, │ │ 973 + reader: *std.Io.Reader, │ │ 974 + writer: *std.Io.Writer, │ │ 975 + cs_cipher: *Cipher, │ │ 976 + sc_cipher: *Cipher, │ │ 977 + rekey: *const RekeyState, │ │ 978 + client_kex_init: []const u8, │ │ 979 +) !void { │ │ 980 + const server_kex_init = try buildServerKexInit(io, allocator); │ │ 981 + defer allocator.free(server_kex_init); │ │ 982 + try sc_cipher.writePacket(io, allocator, writer, server_kex_init); │ │ 983 + │ │ 984 + const negotiated = try exchangeKeys( │ │ 985 + io, │ │ 986 + allocator, │ │ 987 + reader, │ │ 988 + writer, │ │ 989 + .{ .encrypted = .{ .cs = cs_cipher, .sc = sc_cipher } }, │ │ 990 + false, │ │ 991 + rekey.host_key, │ │ 992 + rekey.client_version, │ │ 993 + client_kex_init, │ │ 994 + server_kex_init, │ │ 995 + &rekey.session_id, │ │ 996 + ); │ │ 997 │ │ 998 + // seqnos continue across a rekey unless strict kex was negotiated, │ │ 999 + // which resets them after every NEWKEYS │ │1000 + cs_cipher.setKeys(&negotiated.keys.cs_enc, if (rekey.strict) 0 else cs_cipher.seq);│ │1001 + sc_cipher.setKeys(&negotiated.keys.sc_enc, if (rekey.strict) 0 else sc_cipher.seq);│ │1002 +} │ │1003 + │ │1004 +const NegotiatedKeys = struct { │ │1005 + keys: SessionKeys, │ │1006 + exchange_hash: [Sha256.digest_length]u8, │ │1007 +}; │ │1008 + │ │1009 +// the KEX core shared by the initial exchange and rekeys: pick algorithms, │ │1010 +// run the (hybrid) ECDH round trip, exchange NEWKEYS, derive keys. both │ │1011 +// KEXINIT payloads have already been exchanged by the caller. session_id is │ │1012 +// null on the initial exchange (where it becomes the exchange hash) and the │ │1013 +// original session id on a rekey. │ │1014 +fn exchangeKeys( │ │1015 + io: std.Io, │ │1016 + allocator: std.mem.Allocator, │ │1017 + reader: *std.Io.Reader, │ │1018 + writer: *std.Io.Writer, │ │1019 + transport: KexTransport, │ │1020 + strict: bool, │ │1021 + host_key: *const HostKey, │ │1022 + client_version: []const u8, │ │1023 + client_kex_init: []const u8, │ │1024 + server_kex_init: []const u8, │ │1025 + session_id: ?*const [Sha256.digest_length]u8, │ │1026 +) !NegotiatedKeys { │ │1027 var parsed = try parseClientKexInit(allocator, client_kex_init); │ │1028 defer parsed.deinit(allocator); │ │1029 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1045 │ │1046 // receive KEX_ECDH_INIT (mlkem768x25519 reuses message code 30 — same │ │1047 // wire shape, only the string size differs). │ │ 889 - const ecdh_init = try readPlainKexPacket(allocator, reader, &client_seq); │ │1048 + const ecdh_init = try transport.readPacket(allocator, reader, strict); │ │1049 defer allocator.free(ecdh_init); │ │1050 if (ecdh_init.len < 1 or ecdh_init[0] != SSH_MSG_KEX_ECDH_INIT) return error.UnexpectedMessage;│ │1051 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1121 try writeStringField(&reply, allocator, host_key_blob.items); │ │1122 try writeStringField(&reply, allocator, server_blob); │ │1123 try writeStringField(&reply, allocator, signature_blob.items); │ │ 965 - try writePlainPacket(io, writer, reply.items); │ │1124 + try transport.writePacket(io, allocator, writer, reply.items); │ │1125 │ │ 967 - // NEWKEYS — both sides switch to encrypted mode after this is sent and │ │1126 + // NEWKEYS — both sides switch to the new keys after this is sent and │ │1127 // the peer's NEWKEYS is received. │ │ 969 - try writePlainPacket(io, writer, &[_]u8{SSH_MSG_NEWKEYS}); │ │1128 + try transport.writePacket(io, allocator, writer, &[_]u8{SSH_MSG_NEWKEYS}); │ │1129 │ │ 971 - const peer_newkeys = try readPlainKexPacket(allocator, reader, &client_seq); │ │1130 + const peer_newkeys = try transport.readPacket(allocator, reader, strict); │ │1131 defer allocator.free(peer_newkeys); │ │1132 if (peer_newkeys.len < 1 or peer_newkeys[0] != SSH_MSG_NEWKEYS) return error.UnexpectedMessage;│ │1133 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 975 - // derive session keys per RFC 4253 §7.2. session_id == H for the first │ │ 976 - // KEX. only the encrypt keys are needed for chacha20-poly1305 (no │ │ 977 - // separate MAC/IV). │ │1134 + // derive session keys per RFC 4253 §7.2. only the encrypt keys are │ │1135 + // needed for chacha20-poly1305 (no separate MAC/IV). │ │1136 var keys: SessionKeys = undefined; │ │ 979 - try deriveSessionKeys(allocator, k, &exchange_hash, &exchange_hash, &keys, hybrid); │ │1137 + try deriveSessionKeys(allocator, k, &exchange_hash, if (session_id) |sid| sid else &exchange_hash, &keys, hybrid);│ │1138 │ │ 981 - return .{ │ │ 982 - .cs_key = keys.cs_enc, │ │ 983 - .sc_key = keys.sc_enc, │ │ 984 - .session_id = exchange_hash, │ │ 985 - .next_cs_seq = client_seq, │ │ 986 - }; │ │1139 + return .{ .keys = keys, .exchange_hash = exchange_hash }; │ │1140 } │ │1141 │ │1142 // --------------------------------------------------------------------------- │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │1272 main_key: [32]u8, // K_2 │ │1273 header_key: [32]u8, // K_1 │ │1274 seq: u64, │ │1275 + idle: ?*IdleState = null, │ │1276 │ │1277 pub fn init(key_material: *const [64]u8, initial_seq: u64) Cipher {│ │1278 return .{ │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │1282 }; │ │1283 } │ │1284 │ │1285 + /// swap in new key material after a rekey, leaving the rest of the │ │1286 + /// cipher state alone │ │1287 + fn setKeys(self: *Cipher, key_material: *const [64]u8, seq: u64) void {│ │1288 + self.main_key = key_material[0..32].*; │ │1289 + self.header_key = key_material[32..64].*; │ │1290 + self.seq = seq; │ │1291 + } │ │1292 + │ │1293 fn makeNonce(seq: u64) [8]u8 { │ │1294 var nonce: [8]u8 = undefined; │ │1295 std.mem.writeInt(u64, &nonce, seq, .big); │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │1389 const payload = try allocator.dupe(u8, enc_body[1 .. 1 + payload_len]);│ │1390 │ │1391 self.seq += 1; │ │1392 + if (self.idle) |idle| _ = idle.activity.fetchAdd(1, .monotonic); │ │1393 return payload; │ │1394 } │ │1395 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────╮ │1423 writer: *std.Io.Writer, │ │1424 cs_cipher: *Cipher, │ │1425 sc_cipher: *Cipher, │ │1263 - session_id: *const [Sha256.digest_length]u8, │ │1426 + rekey: *const RekeyState, │ │1427 ) ![fingerprint_len]u8 { │ │1428 // step 1: service request for ssh-userauth → service accept│ │1429 { │ ╰──────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1267 - const req = try readNonIgnoredPacket(cs_cipher, allocator, reader); │ │1430 + const req = try readSessionPacket(io, allocator, reader, writer, cs_cipher, sc_cipher, rekey);│ │1431 defer allocator.free(req); │ │1269 - if (req.len >= 1 and req[0] == SSH_MSG_KEXINIT) { │ │1270 - try sendDisconnect(io, allocator, sc_cipher, writer, "rekey is not supported"); │ │1271 - return error.RekeyUnsupported; │ │1272 - } │ │1432 if (req.len < 1 or req[0] != SSH_MSG_SERVICE_REQUEST) return error.UnexpectedMessage; │ │1433 │ │1434 var req_reader = std.Io.Reader.fixed(req[1..]); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯