╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ ssh.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
587 try req.append(allocator, proto.SSH_MSG_SERVICE_REQUEST); 588 try proto.writeStringField(&req, allocator, "ssh-userauth"); 589 try cs.writePacket(io, cw, req.items); 590 } 591 { 592 const accept = try sc.readPacket(allocator, cr); 593 defer allocator.free(accept); 594 try std.testing.expectEqual(@as(u8, proto.SSH_MSG_SERVICE_ACCEPT), accept[0]); 595 var r = std.Io.Reader.fixed(accept[1..]); 596 const service = try proto.takeStringField(allocator, &r, 64); 597 defer allocator.free(service); 598 try std.testing.expectEqualStrings("ssh-userauth", service); 599 } 600 601 var pubkey_blob: std.ArrayList(u8) = .empty; 602 defer pubkey_blob.deinit(allocator); 603 try proto.writeStringField(&pubkey_blob, allocator, "ssh-ed25519"); 604 try proto.writeStringField(&pubkey_blob, allocator, &user_kp.public_key.bytes); 605 606 { 607 var req: std.ArrayList(u8) = .empty; 608 defer req.deinit(allocator); 609 try req.append(allocator, proto.SSH_MSG_USERAUTH_REQUEST); 610 try proto.writeStringField(&req, allocator, "testuser"); 611 try proto.writeStringField(&req, allocator, "ssh-connection"); 612 try proto.writeStringField(&req, allocator, "publickey"); 613 try req.append(allocator, 0); 614 try proto.writeStringField(&req, allocator, "ssh-ed25519"); 615 try proto.writeStringField(&req, allocator, pubkey_blob.items); 616 try cs.writePacket(io, cw, req.items); 617 } 618 { 619 const pk_ok = try sc.readPacket(allocator, cr); 620 defer allocator.free(pk_ok); 621 try std.testing.expectEqual(@as(u8, proto.SSH_MSG_USERAUTH_PK_OK), pk_ok[0]); 622 var r = std.Io.Reader.fixed(pk_ok[1..]); 623 const algo = try proto.takeStringField(allocator, &r, 64); 624 defer allocator.free(algo); 625 try std.testing.expectEqualStrings("ssh-ed25519", algo); 626 const accepted_blob = try proto.takeStringField(allocator, &r, 4096); 627 defer allocator.free(accepted_blob); 628 try std.testing.expectEqualSlices(u8, pubkey_blob.items, accepted_blob); 629 } 630 631 const wrong_session = [_]u8{0xFF} ** Sha256.digest_length; 632 for ([_]struct { 633 sign_session: []const u8, 634 sign_user: []const u8, 635 send_user: []const u8, 636 expect: u8, 637 }{ 638 .{ .sign_session = &wrong_session, .sign_user = "testuser", .send_user = "testuser", .expect = proto.SSH_MSG_USERAUTH_FAILURE }, 639 .{ .sign_session = &session_id, .sign_user = "alice", .send_user = "bob", .expect = proto.SSH_MSG_USERAUTH_FAILURE }, 640 .{ .sign_session = &session_id, .sign_user = "testuser", .send_user = "testuser", .expect = proto.SSH_MSG_USERAUTH_SUCCESS }, 641 }) |attempt| { 642 var signed: std.ArrayList(u8) = .empty; 643 defer signed.deinit(allocator); 644 try proto.appendPublickeySignedData(&signed, allocator, attempt.sign_session, attempt.sign_user, "ssh-connection", "ssh-ed25519", pubkey_blob.items); 645 const sig = try user_kp.sign(signed.items, null); 646 const sig_bytes = sig.toBytes(); 647 var sig_wire: std.ArrayList(u8) = .empty; 648 defer sig_wire.deinit(allocator); 649 try proto.writeStringField(&sig_wire, allocator, "ssh-ed25519"); 650 try proto.writeStringField(&sig_wire, allocator, &sig_bytes); 651 652 var req: std.ArrayList(u8) = .empty; 653 defer req.deinit(allocator); 654 try req.append(allocator, proto.SSH_MSG_USERAUTH_REQUEST); 655 try proto.writeStringField(&req, allocator, attempt.send_user); 656 try proto.writeStringField(&req, allocator, "ssh-connection"); 657 try proto.writeStringField(&req, allocator, "publickey"); 658 try req.append(allocator, 1); 659 try proto.writeStringField(&req, allocator, "ssh-ed25519"); 660 try proto.writeStringField(&req, allocator, pubkey_blob.items); 661 try proto.writeStringField(&req, allocator, sig_wire.items); 662 try cs.writePacket(io, cw, req.items); 663 664 const reply = try sc.readPacket(allocator, cr); 665 defer allocator.free(reply); 666 try std.testing.expectEqual(attempt.expect, reply[0]); 667 } 668 } 669 670 // open two channels back to back: server confirms the first, then 671 // refuses the second with OPEN_FAILURE and stays running. 672 for ([_]struct { id: u32, expect: u8 }{ 673 .{ .id = 7, .expect = proto.SSH_MSG_CHANNEL_OPEN_CONFIRMATION }, 674 .{ .id = 8, .expect = proto.SSH_MSG_CHANNEL_OPEN_FAILURE }, 675 }) |case| { 676 var req: std.ArrayList(u8) = .empty; 677 defer req.deinit(allocator); 678 try req.append(allocator, proto.SSH_MSG_CHANNEL_OPEN); 679 try proto.writeStringField(&req, allocator, "session"); 680 try proto.writeU32(&req, allocator, case.id); 681 try proto.writeU32(&req, allocator, 1 << 20); 682 try proto.writeU32(&req, allocator, 32768); 683 try cs.writePacket(io, cw, req.items); 684 685 const reply = try sc.readPacket(allocator, cr); 686 defer allocator.free(reply); 687 try std.testing.expectEqual(case.expect, reply[0]); 688 } 689 690 // now break the connection: build any encrypted packet, flip one bit of 691 // its trailing Poly1305 tag, and ship the corrupted bytes. server must 692 // surface MacVerificationFailed out of handleConnection. 693 var req: std.ArrayList(u8) = .empty; 694 defer req.deinit(allocator); 695 try req.append(allocator, proto.SSH_MSG_CHANNEL_REQUEST); 696 try proto.writeU32(&req, allocator, 0); 697 try proto.writeStringField(&req, allocator, "shell"); 698 try req.append(allocator, 0); 699 700 var tmp_buf: [512]u8 = undefined; 701 var sink = std.Io.Writer.fixed(&tmp_buf); 702 try cs.writePacket(io, &sink, req.items); 703 const written = sink.buffered(); 704 tmp_buf[written.len - 1] ^= 0x01; 705 try cw.writeAll(tmp_buf[0..written.len]); 706 try cw.flush(); 707 708 server_future.await(io); 709 try std.testing.expectEqual(@as(?anyerror, error.MacVerificationFailed), server.result); 710 } 711 712 const client_version = "SSH-2.0-haxy_test"; 713 714 // in-memory bidirectional pipe — drives the server side of handleConnection 715 // without a socket. one Pipe carries bytes in one direction. 716 717 const Pipe = std.Io.Queue(u8); 718 719 const PipeReader = struct { 720 interface: std.Io.Reader, 721 io: std.Io, 722 pipe: *Pipe, 723 724 fn init(io: std.Io, pipe: *Pipe, buf: []u8) PipeReader { 725 return .{ 726 .interface = .{ 727 .vtable = &.{ .readVec = readVec, .stream = stream }, 728 .buffer = buf, 729 .seek = 0, 730 .end = 0, 731 }, 732 .io = io, 733 .pipe = pipe, 734 }; 735 } 736 737 fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { 738 // tests don't drive any code path that calls stream, but the vtable 739 // requires a non-default impl. read into a small staging buffer and 740 // forward, so the implementation is correct if it ever fires. 741 const self: *PipeReader = @fieldParentPtr("interface", r); 742 var stage: [256]u8 = undefined; 743 const cap = limit.minInt(stage.len); 744 if (cap == 0) return 0; 745 const n = self.pipe.get(self.io, stage[0..cap], 1) catch |err| switch (err) { 746 error.Closed => return error.EndOfStream, 747 error.Canceled => return error.ReadFailed, 748 }; 749 return try w.write(stage[0..n]); 750 } 751 752 fn readVec(r: *std.Io.Reader, data: [][]u8) std.Io.Reader.Error!usize { 753 const self: *PipeReader = @fieldParentPtr("interface", r); 754 // vtable contract: if data[0] is empty, fill r.buffer[r.end..] instead. 755 const dest = if (data.len == 0 or data[0].len == 0) r.buffer[r.end..] else data[0]; 756 if (dest.len == 0) return 0; 757 const n = self.pipe.get(self.io, dest, 1) catch |err| switch (err) { 758 error.Closed => return error.EndOfStream, 759 error.Canceled => return error.ReadFailed, 760 }; 761 if (data.len == 0 or data[0].len == 0) { 762 r.end += n; 763 return 0; 764 } 765 return n; 766 } 767 }; 768 769 const PipeWriter = struct { 770 interface: std.Io.Writer, 771 io: std.Io, 772 pipe: *Pipe, 773 774 fn init(io: std.Io, pipe: *Pipe, buf: []u8) PipeWriter { 775 return .{ 776 .interface = .{ 777 .vtable = &.{ .drain = drain }, 778 .buffer = buf, 779 }, 780 .io = io, 781 .pipe = pipe, 782 }; 783 } 784 785 fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { 786 const self: *PipeWriter = @fieldParentPtr("interface", w); 787 const buffered = w.buffered(); 788 if (buffered.len > 0) self.pipe.putAll(self.io, buffered) catch return error.WriteFailed; 789 790 var extra: usize = 0; 791 if (data.len > 1) { 792 for (data[0 .. data.len - 1]) |chunk| { 793 if (chunk.len == 0) continue; 794 self.pipe.putAll(self.io, chunk) catch return error.WriteFailed; 795 extra += chunk.len; 796 } 797 } 798 if (data.len > 0) { 799 const pattern = data[data.len - 1]; 800 if (pattern.len > 0) { 801 var i: usize = 0; 802 while (i < splat) : (i += 1) { 803 self.pipe.putAll(self.io, pattern) catch return error.WriteFailed; 804 extra += pattern.len; 805 } 806 } 807 } 808 return w.consume(buffered.len + extra); 809 } 810 }; 811 812 // runs proto.handleConnection in a concurrent task (spawned via std.Io.concurrent). 813 // also acts as the session handler — captures the authenticated key's 814 // fingerprint and returns immediately, leaving runChannelLayer's wrap-up to 815 // send exit-status / EOF / CLOSE. records any error so the test can assert 816 // on it after awaiting the future. 817 const ServerTask = struct { 818 io: std.Io, 819 allocator: std.mem.Allocator, 820 host_key: *const proto.HostKey, 821 reader: *std.Io.Reader, 822 writer: *std.Io.Writer, 823 outgoing: *Pipe, // closed on exit so the client's reads return EOF 824 captured_fp: [proto.fingerprint_len]u8 = undefined, 825 captured_exec_buf: [256]u8 = undefined, 826 captured_exec_len: usize = 0, 827 result: ?anyerror = null, 828 829 pub fn handleSession(self: *ServerTask, sess: *proto.SessionCtx, request: proto.Request) !void { 830 @memcpy(&self.captured_fp, &sess.fingerprint); 831 switch (request) { 832 .exec => |cmd| { 833 // copy out of the protocol's stack-scoped buffer so the 834 // test can assert on it after the handler returns. 835 @memcpy(self.captured_exec_buf[0..cmd.len], cmd); 836 self.captured_exec_len = cmd.len; 837 }, 838 .shell => {}, 839 } 840 } 841 842 fn run(self: *ServerTask) void { 843 defer self.outgoing.close(self.io); 844 proto.handleConnection( 845 self.io, 846 self.allocator, 847 self.reader, 848 self.writer, 849 self.host_key, 850 null, 851 self, 852 ) catch |e| { 853 self.result = e; 854 }; 855 } 856 }; 857 858 // deterministic keys so failures don't depend on RNG state. 859 860 fn newHostKey() !proto.HostKey { 861 const seed = [_]u8{0xAA} ** Ed25519.KeyPair.seed_length; 862 return .{ .keypair = try Ed25519.KeyPair.generateDeterministic(seed) }; 863 } 864 865 fn newUserKeypair() !Ed25519.KeyPair { 866 const seed = [_]u8{0xBB} ** Ed25519.KeyPair.seed_length; 867 return try Ed25519.KeyPair.generateDeterministic(seed); 868 } 869 870 fn newClientEphemeral() !X25519.KeyPair { 871 const seed = [_]u8{0xCC} ** X25519.seed_length; 872 return try X25519.KeyPair.generateDeterministic(seed); 873 } 874