╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ ssh.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
740 try std.testing.expectEqual(@as(u8, proto.SSH_MSG_SERVICE_ACCEPT), accept[0]); 741 var r = std.Io.Reader.fixed(accept[1..]); 742 const service = try proto.takeStringField(allocator, &r, 64); 743 defer allocator.free(service); 744 try std.testing.expectEqualStrings("ssh-userauth", service); 745 } 746 747 var pubkey_blob: std.ArrayList(u8) = .empty; 748 defer pubkey_blob.deinit(allocator); 749 try proto.writeStringField(&pubkey_blob, allocator, "ssh-ed25519"); 750 try proto.writeStringField(&pubkey_blob, allocator, &user_kp.public_key.bytes); 751 752 { 753 var req: std.ArrayList(u8) = .empty; 754 defer req.deinit(allocator); 755 try req.append(allocator, proto.SSH_MSG_USERAUTH_REQUEST); 756 try proto.writeStringField(&req, allocator, "testuser"); 757 try proto.writeStringField(&req, allocator, "ssh-connection"); 758 try proto.writeStringField(&req, allocator, "publickey"); 759 try req.append(allocator, 0); 760 try proto.writeStringField(&req, allocator, "ssh-ed25519"); 761 try proto.writeStringField(&req, allocator, pubkey_blob.items); 762 try cs.writePacket(io, cw, req.items); 763 } 764 { 765 const pk_ok = try sc.readPacket(allocator, cr); 766 defer allocator.free(pk_ok); 767 try std.testing.expectEqual(@as(u8, proto.SSH_MSG_USERAUTH_PK_OK), pk_ok[0]); 768 var r = std.Io.Reader.fixed(pk_ok[1..]); 769 const algo = try proto.takeStringField(allocator, &r, 64); 770 defer allocator.free(algo); 771 try std.testing.expectEqualStrings("ssh-ed25519", algo); 772 const accepted_blob = try proto.takeStringField(allocator, &r, 4096); 773 defer allocator.free(accepted_blob); 774 try std.testing.expectEqualSlices(u8, pubkey_blob.items, accepted_blob); 775 } 776 777 const wrong_session = [_]u8{0xFF} ** Sha256.digest_length; 778 for ([_]struct { 779 sign_session: []const u8, 780 sign_user: []const u8, 781 send_user: []const u8, 782 expect: u8, 783 }{ 784 .{ .sign_session = &wrong_session, .sign_user = "testuser", .send_user = "testuser", .expect = proto.SSH_MSG_USERAUTH_FAILURE }, 785 .{ .sign_session = &session_id, .sign_user = "alice", .send_user = "bob", .expect = proto.SSH_MSG_USERAUTH_FAILURE }, 786 .{ .sign_session = &session_id, .sign_user = "testuser", .send_user = "testuser", .expect = proto.SSH_MSG_USERAUTH_SUCCESS }, 787 }) |attempt| { 788 var signed: std.ArrayList(u8) = .empty; 789 defer signed.deinit(allocator); 790 try proto.appendPublickeySignedData(&signed, allocator, attempt.sign_session, attempt.sign_user, "ssh-connection", "ssh-ed25519", pubkey_blob.items); 791 const sig = try user_kp.sign(signed.items, null); 792 const sig_bytes = sig.toBytes(); 793 var sig_wire: std.ArrayList(u8) = .empty; 794 defer sig_wire.deinit(allocator); 795 try proto.writeStringField(&sig_wire, allocator, "ssh-ed25519"); 796 try proto.writeStringField(&sig_wire, allocator, &sig_bytes); 797 798 var req: std.ArrayList(u8) = .empty; 799 defer req.deinit(allocator); 800 try req.append(allocator, proto.SSH_MSG_USERAUTH_REQUEST); 801 try proto.writeStringField(&req, allocator, attempt.send_user); 802 try proto.writeStringField(&req, allocator, "ssh-connection"); 803 try proto.writeStringField(&req, allocator, "publickey"); 804 try req.append(allocator, 1); 805 try proto.writeStringField(&req, allocator, "ssh-ed25519"); 806 try proto.writeStringField(&req, allocator, pubkey_blob.items); 807 try proto.writeStringField(&req, allocator, sig_wire.items); 808 try cs.writePacket(io, cw, req.items); 809 810 const reply = try sc.readPacket(allocator, cr); 811 defer allocator.free(reply); 812 try std.testing.expectEqual(attempt.expect, reply[0]); 813 } 814 } 815 816 // open two channels back to back: server confirms the first, then 817 // refuses the second with OPEN_FAILURE and stays running. 818 for ([_]struct { id: u32, expect: u8 }{ 819 .{ .id = 7, .expect = proto.SSH_MSG_CHANNEL_OPEN_CONFIRMATION }, 820 .{ .id = 8, .expect = proto.SSH_MSG_CHANNEL_OPEN_FAILURE }, 821 }) |case| { 822 var req: std.ArrayList(u8) = .empty; 823 defer req.deinit(allocator); 824 try req.append(allocator, proto.SSH_MSG_CHANNEL_OPEN); 825 try proto.writeStringField(&req, allocator, "session"); 826 try proto.writeU32(&req, allocator, case.id); 827 try proto.writeU32(&req, allocator, 1 << 20); 828 try proto.writeU32(&req, allocator, 32768); 829 try cs.writePacket(io, cw, req.items); 830 831 const reply = try sc.readPacket(allocator, cr); 832 defer allocator.free(reply); 833 try std.testing.expectEqual(case.expect, reply[0]); 834 } 835 836 // now break the connection: build any encrypted packet, flip one bit of 837 // its trailing Poly1305 tag, and ship the corrupted bytes. server must 838 // surface MacVerificationFailed out of handleConnection. 839 var req: std.ArrayList(u8) = .empty; 840 defer req.deinit(allocator); 841 try req.append(allocator, proto.SSH_MSG_CHANNEL_REQUEST); 842 try proto.writeU32(&req, allocator, 0); 843 try proto.writeStringField(&req, allocator, "shell"); 844 try req.append(allocator, 0); 845 846 var tmp_buf: [512]u8 = undefined; 847 var sink = std.Io.Writer.fixed(&tmp_buf); 848 try cs.writePacket(io, &sink, req.items); 849 const written = sink.buffered(); 850 tmp_buf[written.len - 1] ^= 0x01; 851 try cw.writeAll(tmp_buf[0..written.len]); 852 try cw.flush(); 853 854 server_future.await(io); 855 try std.testing.expectEqual(@as(?anyerror, error.MacVerificationFailed), server.result); 856 } 857 858 const client_version = "SSH-2.0-haxy_test"; 859 860 // in-memory bidirectional pipe — drives the server side of handleConnection 861 // without a socket. one Pipe carries bytes in one direction. 862 863 const Pipe = std.Io.Queue(u8); 864 865 const PipeReader = struct { 866 interface: std.Io.Reader, 867 io: std.Io, 868 pipe: *Pipe, 869 870 fn init(io: std.Io, pipe: *Pipe, buf: []u8) PipeReader { 871 return .{ 872 .interface = .{ 873 .vtable = &.{ .readVec = readVec, .stream = stream }, 874 .buffer = buf, 875 .seek = 0, 876 .end = 0, 877 }, 878 .io = io, 879 .pipe = pipe, 880 }; 881 } 882 883 fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { 884 // tests don't drive any code path that calls stream, but the vtable 885 // requires a non-default impl. read into a small staging buffer and 886 // forward, so the implementation is correct if it ever fires. 887 const self: *PipeReader = @fieldParentPtr("interface", r); 888 var stage: [256]u8 = undefined; 889 const cap = limit.minInt(stage.len); 890 if (cap == 0) return 0; 891 const n = self.pipe.get(self.io, stage[0..cap], 1) catch |err| switch (err) { 892 error.Closed => return error.EndOfStream, 893 error.Canceled => return error.ReadFailed, 894 }; 895 return try w.write(stage[0..n]); 896 } 897 898 fn readVec(r: *std.Io.Reader, data: [][]u8) std.Io.Reader.Error!usize { 899 const self: *PipeReader = @fieldParentPtr("interface", r); 900 // vtable contract: if data[0] is empty, fill r.buffer[r.end..] instead. 901 const dest = if (data.len == 0 or data[0].len == 0) r.buffer[r.end..] else data[0]; 902 if (dest.len == 0) return 0; 903 const n = self.pipe.get(self.io, dest, 1) catch |err| switch (err) { 904 error.Closed => return error.EndOfStream, 905 error.Canceled => return error.ReadFailed, 906 }; 907 if (data.len == 0 or data[0].len == 0) { 908 r.end += n; 909 return 0; 910 } 911 return n; 912 } 913 }; 914 915 const PipeWriter = struct { 916 interface: std.Io.Writer, 917 io: std.Io, 918 pipe: *Pipe, 919 920 fn init(io: std.Io, pipe: *Pipe, buf: []u8) PipeWriter { 921 return .{ 922 .interface = .{ 923 .vtable = &.{ .drain = drain }, 924 .buffer = buf, 925 }, 926 .io = io, 927 .pipe = pipe, 928 }; 929 } 930 931 fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { 932 const self: *PipeWriter = @fieldParentPtr("interface", w); 933 const buffered = w.buffered(); 934 if (buffered.len > 0) self.pipe.putAll(self.io, buffered) catch return error.WriteFailed; 935 936 var extra: usize = 0; 937 if (data.len > 1) { 938 for (data[0 .. data.len - 1]) |chunk| { 939 if (chunk.len == 0) continue; 940 self.pipe.putAll(self.io, chunk) catch return error.WriteFailed; 941 extra += chunk.len; 942 } 943 } 944 if (data.len > 0) { 945 const pattern = data[data.len - 1]; 946 if (pattern.len > 0) { 947 var i: usize = 0; 948 while (i < splat) : (i += 1) { 949 self.pipe.putAll(self.io, pattern) catch return error.WriteFailed; 950 extra += pattern.len; 951 } 952 } 953 } 954 return w.consume(buffered.len + extra); 955 } 956 }; 957 958 // runs proto.handleConnection in a concurrent task (spawned via std.Io.concurrent). 959 // also acts as the session handler — captures the authenticated key's 960 // fingerprint and returns immediately, leaving runChannelLayer's wrap-up to 961 // send exit-status / EOF / CLOSE. records any error so the test can assert 962 // on it after awaiting the future. 963 const ServerTask = struct { 964 io: std.Io, 965 allocator: std.mem.Allocator, 966 host_key: *const proto.HostKey, 967 reader: *std.Io.Reader, 968 writer: *std.Io.Writer, 969 outgoing: *Pipe, // closed on exit so the client's reads return EOF 970 captured_fp: [proto.fingerprint_len]u8 = undefined, 971 captured_exec_buf: [256]u8 = undefined, 972 captured_exec_len: usize = 0, 973 result: ?anyerror = null, 974 975 pub fn handleSession(self: *ServerTask, sess: *proto.SessionCtx, request: proto.Request) !void { 976 @memcpy(&self.captured_fp, &sess.fingerprint); 977 switch (request) { 978 .exec => |cmd| { 979 // copy out of the protocol's stack-scoped buffer so the 980 // test can assert on it after the handler returns. 981 @memcpy(self.captured_exec_buf[0..cmd.len], cmd); 982 self.captured_exec_len = cmd.len; 983 }, 984 .shell => {}, 985 } 986 } 987 988 fn run(self: *ServerTask) void { 989 defer self.outgoing.close(self.io); 990 proto.handleConnection( 991 self.io, 992 self.allocator, 993 self.reader, 994 self.writer, 995 self.host_key, 996 null, 997 self, 998 ) catch |e| { 999 self.result = e; 1000 }; 1001 } 1002 }; 1003 1004 // deterministic keys so failures don't depend on RNG state. 1005 1006 fn newHostKey() !proto.HostKey { 1007 const seed = [_]u8{0xAA} ** Ed25519.KeyPair.seed_length; 1008 return .{ .keypair = try Ed25519.KeyPair.generateDeterministic(seed) }; 1009 } 1010 1011 fn newUserKeypair() !Ed25519.KeyPair { 1012 const seed = [_]u8{0xBB} ** Ed25519.KeyPair.seed_length; 1013 return try Ed25519.KeyPair.generateDeterministic(seed); 1014 } 1015 1016 fn newClientEphemeral() !X25519.KeyPair { 1017 const seed = [_]u8{0xCC} ** X25519.seed_length; 1018 return try X25519.KeyPair.generateDeterministic(seed); 1019 } 1020