╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │342 try discardChannelData(self.io, self.allocator, self.sc_cipher, self.writer, self.channel, packet, true);│ │343 }, │ │344 SSH_MSG_CHANNEL_WINDOW_ADJUST => try handleWindowAdjust(self.channel, packet), │ │341 - SSH_MSG_KEXINIT => { │ │342 - try sendDisconnect(self.io, self.allocator, self.sc_cipher, self.writer, "rekey is not supported"); │ │343 - return error.RekeyUnsupported; │ │344 - }, │ │345 SSH_MSG_CHANNEL_REQUEST => { │ │346 var r = std.Io.Reader.fixed(packet[1..]); │ │347 const recipient_channel = try r.takeInt(u32, .big); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │562 /// pub fn handleSession(self, sess: *SessionCtx, request: Request) anyerror!void│ │563 /// invoked once the channel is open and a shell/exec request has arrived. │ │564 /// │ │565 -/// `reader` and `writer` are the bidirectional byte stream │ │565 +/// `reader` and `writer` are the bidirectional byte stream. `idle`, if │ │566 +/// given, is bumped on packet reads for the host's idle watchdog. │ │567 pub fn handleConnection( │ │568 io: std.Io, │ │569 allocator: std.mem.Allocator, │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │570 reader: *std.Io.Reader, │ │571 writer: *std.Io.Writer, │ │572 host_key: *const HostKey, │ │573 + idle: ?*IdleState, │ │574 handler: anytype, │ │575 ) !void { │ │576 const client_version = try exchangeVersions(allocator, reader, writer);│ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │578 │ │579 const kex = try runKex(io, allocator, reader, writer, host_key, client_version); │ │580 │ │579 - // post-KEX everything is encrypted with chacha20-poly1305@openssh.com. │ │580 - // both sides started counting packet seqnos at 0. we sent exactly │ │581 - // KEXINIT, KEX_ECDH_REPLY and NEWKEYS, so our first encrypted packet │ │582 - // uses seqno 3; the client's count includes any skipped IGNORE/DEBUG. │ │581 + // post-KEX everything is encrypted with chacha20-poly1305@openssh.com │ │582 var cs_cipher = Cipher.init(&kex.cs_key, kex.next_cs_seq); │ │584 - var sc_cipher = Cipher.init(&kex.sc_key, 3); │ │583 + var sc_cipher = Cipher.init(&kex.sc_key, kex.next_sc_seq); │ │584 + cs_cipher.idle = idle; │ │585 + │ │586 + const rekey = RekeyState{ │ │587 + .host_key = host_key, │ │588 + .client_version = client_version, │ │589 + .session_id = kex.session_id, │ │590 + .strict = kex.strict, │ │591 + }; │ │592 │ │586 - const fingerprint = try runAuth(io, allocator, reader, writer, &cs_cipher, &sc_cipher, &kex.session_id); │ │593 + const fingerprint = try runAuth(io, allocator, reader, writer, &cs_cipher, &sc_cipher, &rekey); │ │594 │ │588 - try runChannelLayer(io, allocator, reader, writer, &cs_cipher, &sc_cipher, &fingerprint, handler); │ │595 + try runChannelLayer(io, allocator, reader, writer, &cs_cipher, &sc_cipher, &rekey, &fingerprint, handler);│ │596 } │ │597 │ │598 // --------------------------------------------------------------------------- │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │607 try writer.writeAll(server_version ++ "\r\n"); │ │608 try writer.flush(); │ │609 │ │603 - // the spec allows the client to send arbitrary comment lines before its │ │604 - // banner; skip anything that isn't an "SSH-2.0-…" / "SSH-1.99-…" line. │ │605 - while (true) { │ │610 + // the spec allows the client to send comment lines before its banner; │ │611 + // skip anything that isn't an "SSH-2.0-…" / "SSH-1.99-…" line, up to a cap │ │612 + // so a garbage-spewing peer can't hold the connection open forever. │ │613 + var lines: u32 = 0; │ │614 + while (lines < max_banner_lines) : (lines += 1) { │ │615 const line = (try reader.takeDelimiter('\n')) orelse return error.UnexpectedEof; │ │616 const trimmed = if (line.len > 0 and line[line.len - 1] == '\r') line[0 .. line.len - 1] else line;│ │617 if (std.mem.startsWith(u8, trimmed, "SSH-2.0-") or std.mem.startsWith(u8, trimmed, "SSH-1.99-")) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │618 return try allocator.dupe(u8, trimmed); │ │619 } │ │620 } │ │621 + return error.TooManyBannerLines; │ │622 } │ │623 │ │624 // ---------------------------------------------------------------------------│ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │660 return msg_type == SSH_MSG_IGNORE or msg_type == SSH_MSG_UNIMPLEMENTED or msg_type == SSH_MSG_DEBUG; │ │661 } │ │662 │ │653 -// read a plaintext packet, skipping ignorable messages. counts every packet │ │654 -// consumed in `seq` so the caller knows the peer's next sequence number. │ │655 -fn readPlainKexPacket(allocator: std.mem.Allocator, reader: *std.Io.Reader, seq: *u64) ![]u8 { │ │656 - while (true) { │ │657 - const packet = try readPlainPacket(allocator, reader); │ │658 - seq.* += 1; │ │659 - if (packet.len >= 1 and isIgnorableMsg(packet[0])) { │ │660 - allocator.free(packet); │ │661 - continue; │ │663 +// transport for KEX packets: the initial exchange runs in plaintext, a rekey │ │664 +// runs under the current session ciphers. │ │665 +const KexTransport = union(enum) { │ │666 + plain: *u64, // counts packets read so the caller can seed cipher seqnos │ │667 + encrypted: struct { cs: *Cipher, sc: *Cipher }, │ │668 + │ │669 + // read one packet, skipping ignorable messages — unless strict kex is in │ │670 + // force, which bans them mid-handshake │ │671 + fn readPacket(self: KexTransport, allocator: std.mem.Allocator, reader: *std.Io.Reader, strict: bool) ![]u8 { │ │672 + while (true) { │ │673 + const packet = switch (self) { │ │674 + .plain => |count| packet: { │ │675 + const packet = try readPlainPacket(allocator, reader); │ │676 + count.* += 1; │ │677 + break :packet packet; │ │678 + }, │ │679 + .encrypted => |ciphers| try ciphers.cs.readPacket(allocator, reader), │ │680 + }; │ │681 + if (packet.len >= 1 and isIgnorableMsg(packet[0])) { │ │682 + allocator.free(packet); │ │683 + if (strict) return error.StrictKexViolation; │ │684 + continue; │ │685 + } │ │686 + return packet; │ │687 + } │ │688 + } │ │689 + │ │690 + fn writePacket(self: KexTransport, io: std.Io, allocator: std.mem.Allocator, writer: *std.Io.Writer, payload: []const u8) !void {│ │691 + switch (self) { │ │692 + .plain => try writePlainPacket(io, writer, payload), │ │693 + .encrypted => |ciphers| try ciphers.sc.writePacket(io, allocator, writer, payload), │ │694 } │ │663 - return packet; │ │695 } │ │665 -} │ │696 +}; │ │697 │ │667 -// read an encrypted packet, skipping ignorable messages │ │668 -fn readNonIgnoredPacket(cipher: *Cipher, allocator: std.mem.Allocator, reader: *std.Io.Reader) ![]u8 { │ │698 +// read one encrypted packet for the auth and channel layers, transparently │ │699 +// skipping ignorable messages and servicing client-initiated rekeys. │ │700 +fn readSessionPacket( │ │701 + io: std.Io, │ │702 + allocator: std.mem.Allocator, │ │703 + reader: *std.Io.Reader, │ │704 + writer: *std.Io.Writer, │ │705 + cs_cipher: *Cipher, │ │706 + sc_cipher: *Cipher, │ │707 + rekey: *const RekeyState, │ │708 +) ![]u8 { │ │709 while (true) { │ │670 - const packet = try cipher.readPacket(allocator, reader); │ │710 + const packet = try cs_cipher.readPacket(allocator, reader); │ │711 if (packet.len >= 1 and isIgnorableMsg(packet[0])) { │ │712 allocator.free(packet); │ │713 + continue; │ │714 + } │ │715 + if (packet.len >= 1 and packet[0] == SSH_MSG_KEXINIT) { │ │716 + defer allocator.free(packet); │ │717 + try runRekey(io, allocator, reader, writer, cs_cipher, sc_cipher, rekey, packet); │ │718 continue; │ │719 } │ │720 return packet; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │790 │ │791 pub const our_kex_algos = [_][]const u8{ "mlkem768x25519-sha256", "curve25519-sha256", "curve25519-sha256@libssh.org" };│ │792 │ │793 +// strict kex markers (Terrapin / CVE-2023-48795 mitigation). pseudo-algorithms │ │794 +// carried in the initial KEXINIT's kex list: we advertise the -s form, the │ │795 +// client advertises the -c form. they are never selected as an actual kex │ │796 +// algorithm because the matcher only picks from our_kex_algos. │ │797 +pub const kex_strict_server = "kex-strict-s-v00@openssh.com"; │ │798 +pub const kex_strict_client = "kex-strict-c-v00@openssh.com"; │ │799 + │ │800 const MLKem768 = std.crypto.kem.ml_kem.MLKem768; │ │801 const hybrid_client_blob_len = MLKem768.PublicKey.encoded_length + X25519.public_length; // 1216 │ │802 const hybrid_server_blob_len = MLKem768.ciphertext_length + X25519.public_length; // 1120 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │839 } │ │840 │ │841 fn buildServerKexInit(io: std.Io, allocator: std.mem.Allocator) ![]u8 { │ │790 - return buildKexInit(io, allocator, &our_kex_algos, &our_host_key_algos, &our_ciphers); │ │842 + return buildKexInit(io, allocator, &(our_kex_algos ++ [_][]const u8{kex_strict_server}), &our_host_key_algos, &our_ciphers);│ │843 } │ │844 │ │845 const ParsedKexInit = struct { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │896 cs_key: [64]u8, // client → server cipher key material (K_2 || K_1) │ │897 sc_key: [64]u8, // server → client cipher key material │ │898 session_id: [Sha256.digest_length]u8, │ │847 - next_cs_seq: u64, // packets the client has sent, including skipped ones│ │899 + next_cs_seq: u64, │ │900 + next_sc_seq: u64, │ │901 + strict: bool, │ │902 }; │ │903 │ │904 fn runKex( │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯