╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │1170 try conn.sc_cipher.writePacket(conn.io, conn.writer, &.{server_kex_init});│ │1171 │ │1261 - var negotiated = try exchangeKeys( │ │1172 + var keys: SessionKeys = undefined; │ │1173 + defer std.crypto.secureZero(u8, std.mem.asBytes(&keys)); │ │1174 + _ = try exchangeKeys( │ │1175 conn.io, │ │1176 conn.allocator, │ │1177 conn.reader, │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │1184 server_kex_init, │ │1185 &parsed, │ │1186 &conn.rekey.session_id, │ │1187 + &keys, │ │1188 ); │ │1275 - defer std.crypto.secureZero(u8, std.mem.asBytes(&negotiated.keys));│ │1189 │ │1190 // seqnos continue across a rekey unless strict kex was negotiated,│ │1191 // which resets them after every NEWKEYS │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1279 - conn.cs_cipher.setKeys(&negotiated.keys.cs_enc, if (conn.rekey.strict) 0 else conn.cs_cipher.seq);│ │1280 - conn.sc_cipher.setKeys(&negotiated.keys.sc_enc, if (conn.rekey.strict) 0 else conn.sc_cipher.seq);│ │1192 + conn.cs_cipher = .init(&keys.cs_enc, if (conn.rekey.strict) 0 else conn.cs_cipher.seq); │ │1193 + conn.sc_cipher = .init(&keys.sc_enc, if (conn.rekey.strict) 0 else conn.sc_cipher.seq); │ │1194 } │ │1282 - │ │1283 -const NegotiatedKeys = struct { │ │1284 - keys: SessionKeys, │ │1285 - exchange_hash: [Sha256.digest_length]u8, │ │1286 -}; │ │1195 │ │1196 // the KEX core shared by the initial exchange and rekeys: pick algorithms, │ │1197 // run the (hybrid) ECDH round trip, exchange NEWKEYS, derive keys. both │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │1198 // KEXINIT payloads have already been exchanged by the caller, which also owns │ │1199 // `parsed`; the client's raw payload comes along too because the exchange hash│ │1200 // covers it verbatim. session_id is null on the initial exchange (where it │ │1293 -// becomes the exchange hash) and the original session id on a rekey. │ │1201 +// becomes the exchange hash) and the original session id on a rekey. fills │ │1202 +// `keys` and returns the exchange hash. │ │1203 fn exchangeKeys( │ │1204 io: std.Io, │ │1205 allocator: std.mem.Allocator, │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1213 server_kex_init: []const u8, │ │1214 parsed: *const ParsedKexInit, │ │1215 session_id: ?*const [Sha256.digest_length]u8, │ │1307 -) !NegotiatedKeys { │ │1308 - // pick the first kex algo in the client's preference list that we also │ │1309 - // support (RFC 4253 §7.1.3). │ │1310 - const kex_algo = blk: { │ │1311 - var iter = std.mem.splitScalar(u8, parsed.kex_algos, ','); │ │1312 - while (iter.next()) |name| { │ │1313 - for (our_kex_algos) |opt| { │ │1314 - if (std.mem.eql(u8, name, opt)) break :blk opt; │ │1315 - } │ │1316 - } │ │1317 - return error.NoCommonKexAlgorithm; │ │1318 - }; │ │1319 - if (!nameListContainsAny(parsed.host_key_algos, &our_host_key_algos)) return error.NoCommonHostKeyAlgorithm; │ │1320 - if (!nameListContainsAny(parsed.cs_cipher, &our_ciphers)) return error.NoCommonCipher; │ │1321 - if (!nameListContainsAny(parsed.sc_cipher, &our_ciphers)) return error.NoCommonCipher; │ │1216 + keys: *SessionKeys, │ │1217 +) ![Sha256.digest_length]u8 { │ │1218 + const kex_algo = firstCommonName(parsed.kex_algos, &our_kex_algos) orelse return error.NoCommonKexAlgorithm; │ │1219 + if (firstCommonName(parsed.host_key_algos, &our_host_key_algos) == null) return error.NoCommonHostKeyAlgorithm;│ │1220 + if (firstCommonName(parsed.cs_cipher, &our_ciphers) == null) return error.NoCommonCipher; │ │1221 + if (firstCommonName(parsed.sc_cipher, &our_ciphers) == null) return error.NoCommonCipher; │ │1222 const hybrid = std.mem.eql(u8, kex_algo, "mlkem768x25519-sha256"); │ │1223 │ │1224 // a client may follow its KEXINIT with a kex packet for its guess. the │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1241 // wire shape, only the string size differs). │ │1242 const ecdh_init = try transport.readPacket(allocator, reader, strict); │ │1243 defer allocator.free(ecdh_init); │ │1344 - if (ecdh_init.len < 1 or ecdh_init[0] != SSH_MSG_KEX_ECDH_INIT) return error.UnexpectedMessage;│ │1244 + if (ecdh_init[0] != SSH_MSG_KEX_ECDH_INIT) return error.UnexpectedMessage; │ │1245 │ │1246 var ecdh_init_reader = std.Io.Reader.fixed(ecdh_init[1..]); │ │1247 const expected_client_len: u32 = if (hybrid) hybrid_client_blob_len else X25519.public_length; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1326 │ │1327 const peer_newkeys = try transport.readPacket(allocator, reader, strict); │ │1328 defer allocator.free(peer_newkeys); │ │1429 - if (peer_newkeys.len < 1 or peer_newkeys[0] != SSH_MSG_NEWKEYS) return error.UnexpectedMessage;│ │1329 + if (peer_newkeys[0] != SSH_MSG_NEWKEYS) return error.UnexpectedMessage; │ │1330 │ │1331 // derive session keys per RFC 4253 §7.2. only the encrypt keys are │ │1332 // needed for chacha20-poly1305 (no separate MAC/IV). │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1433 - var keys: SessionKeys = undefined; │ │1434 - defer std.crypto.secureZero(u8, std.mem.asBytes(&keys)); │ │1435 - try deriveSessionKeys(allocator, &k, &exchange_hash, if (session_id) |sid| sid else &exchange_hash, &keys, hybrid);│ │1436 - │ │1437 - return .{ .keys = keys, .exchange_hash = exchange_hash }; │ │1333 + try deriveSessionKeys(allocator, &k, &exchange_hash, if (session_id) |sid| sid else &exchange_hash, keys, hybrid); │ │1334 + return exchange_hash; │ │1335 } │ │1336 │ │1337 // --------------------------------------------------------------------------- │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │1472 }; │ │1473 } │ │1474 │ │1578 - /// swap in new key material after a rekey, leaving the rest of the │ │1579 - /// cipher state alone │ │1580 - fn setKeys(self: *Cipher, key_material: *const [64]u8, seq: u64) void {│ │1581 - self.main_key = key_material[0..32].*; │ │1582 - self.header_key = key_material[32..64].*; │ │1583 - self.seq = seq; │ │1584 - } │ │1585 - │ │1475 fn makeNonce(seq: u64) [8]u8 { │ │1476 var nonce: [8]u8 = undefined; │ │1477 std.mem.writeInt(u64, &nonce, seq, .big); │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1593 const padding_len = body[0]; │ │1594 if (padding_len < 4 or 1 + @as(u32, padding_len) > body_len) return error.InvalidPadding;│ │1595 const payload_len = body_len - 1 - @as(u32, padding_len); │ │1596 + // a payload starts with its message type │ │1597 + if (payload_len == 0) return error.EmptyPacket; │ │1598 │ │1599 // shift the payload over the padding_length byte, then release the │ │1600 // trailing padding (caller owns the result) │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯