╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │393 .allowed => |value| value, │ │394 .create => |creator| return runCreatingPush(handler, sess, &creator, owner_repo.name, protocol_version, &reader.interface, &writer.interface),│ │395 .denied => return switch (parsed.service) { │ │396 - .upload_pack => writeError(sess, "unauthorized: this SSH key cannot read this repo"), │ │397 - .receive_pack => writeError(sess, "unauthorized: this SSH key cannot push to this repo"), │ │396 + .upload_pack => writeKeyError(handler, sess, "unauthorized: the account with this SSH key cannot read this repo"), │ │397 + .receive_pack => writeKeyError(handler, sess, "unauthorized: the account with this SSH key cannot push to this repo"), │ │398 }, │ │399 .not_found => return writeError(sess, "repo not found"), │ │400 }; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │497 return writeError(sess, "invalid patch draft"); │ │498 if (user.removed) return writeError(sess, "invalid patch draft"); │ │499 if (service == .receive_pack and !try evt.User.holdsKey(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, &forker_id, &sess.fingerprint))│ │500 - return writeError(sess, "unauthorized: this SSH key is not registered to the patch author"); │ │500 + return writeKeyError(handler, sess, "unauthorized: this SSH key is not registered to the patch author"); │ │501 │ │502 const target_owner_id = try evt.parseEventId(&fork_record.event.repo_user_id); │ │503 const target_id = try evt.parseEventId(&fork_record.event.repo_id); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │517 const owner = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, &target_owner_id)) orelse unreachable; │ │518 break :readable try authorizeRepoKey(io, &admin_arena, handler.admin_repo_path, handler.server.users_dir, owner.event.name, value.repo.event.name, .upload_pack, &sess.fingerprint) == .allowed;│ │519 } else try evt.User.holdsKey(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, &forker_id, &sess.fingerprint); │ │520 - if (!readable) return writeError(sess, "unauthorized: this SSH key cannot read this patch draft"); │ │520 + if (!readable) return writeKeyError(handler, sess, "unauthorized: the account with this SSH key cannot read this patch draft"); │ │521 } │ │522 │ │523 const draft_path = try fork.forkPath(allocator, handler.server.users_dir, &forker_id, &fork_id); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │678 const body = "ERR haxy ssh: " ++ msg; │ │679 var packet: [body.len + 4]u8 = undefined; │ │680 const line = try std.fmt.bufPrint(&packet, "{x:0>4}{s}", .{ packet.len, body }); │ │681 + try sess.writeBytes(line); │ │682 + try sess.exit(1); │ │683 +} │ │684 + │ │685 +// a refusal naming the key the client presented. a key on no account gets its │ │686 +// own message, since clients send whichever key they offer first. │ │687 +fn writeKeyError(handler: *const SessionHandler, sess: *ssh.SessionCtx, comptime msg: []const u8) !void { │ │688 + const io = sess.conn.io; │ │689 + const allocator = sess.conn.allocator; │ │690 + const known = known: { │ │691 + var arena = std.heap.ArenaAllocator.init(allocator); │ │692 + defer arena.deinit(); │ │693 + var admin = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = handler.admin_repo_path }); │ │694 + defer admin.deinit(io, allocator); │ │695 + const moment = try evt.currentMoment(evt.admin_repo_opts, &admin); │ │696 + break :known (try evt.User.readIdsByFingerprint(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &sess.fingerprint)).len > 0; │ │697 + }; │ │698 + if (known) return writeFingerprintError(sess, msg); │ │699 + return writeFingerprintError(sess, "unauthorized: this SSH key isn't added to any haxy account. if you have several keys, your SSH client may be offering a different one first");│ │700 +} │ │701 + │ │702 +// an error line ending with the presented key's fingerprint │ │703 +fn writeFingerprintError(sess: *ssh.SessionCtx, comptime msg: []const u8) !void { │ │704 + const prefix = "ERR haxy ssh: " ++ msg ++ " (key "; │ │705 + var packet: [4 + prefix.len + ssh.fingerprint_len + 1]u8 = undefined; │ │706 + const line = try std.fmt.bufPrint(&packet, "{x:0>4}" ++ prefix ++ "{s})", .{ packet.len, &sess.fingerprint }); │ │707 try sess.writeBytes(line); │ │708 try sess.exit(1); │ │709 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────╮ │src/ui/UserKeys.zig│ ╰───────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 99 var row = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .horiz }); │ │100 errdefer row.deinit(allocator); │ │101 { │ │102 + // the fingerprint ssh tools print, which an ssh refusal names too │ │103 + const fingerprint = if (evt.User.fingerprintOfKey(key)) |value| try aa.print(" {s} ", .{&value}) else "";│ │104 var text_input = try wgt.TextInput.init(allocator, .{ │ │105 .top_label = .{ .text = try aa.print(" {s} ", .{keyLabel(key)}) }, │ │106 + .bottom_label = .{ .text = fingerprint }, │ │107 .visible_width = input_width, │ │108 .round_corners = true, │ │109 .read_only = true, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯