From 836b0f79216704149efe4bdfd6f8a71091a0ed12 Mon Sep 17 00:00:00 2001 From: JGZYES Date: Fri, 14 Aug 2026 14:35:24 +0800 Subject: [PATCH] =?UTF-8?q?keyboard-interactive=20=E8=AE=A4=E8=AF=81(RFC?= =?UTF-8?q?=204256):=20=E5=AE=A2=E6=88=B7=E7=AB=AF=20password=20=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E5=90=8E=20fallback=20=E5=88=B0=20ki(=E6=94=B6?= =?UTF-8?q?=E5=8F=91=20INFO=5FREQUEST/RESPONSE=20=E4=BB=A5=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E5=93=8D=E5=BA=94);=20=E6=9C=8D=E5=8A=A1=E7=AB=AF?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E5=90=AB=20keyboard-interactive=20=E5=B9=B6?= =?UTF-8?q?=E5=A4=8D=E7=94=A8=E7=94=A8=E6=88=B7=E5=BA=93=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C,=20psshd=20-kbdint=20=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E4=BB=85=E6=8E=A5=E5=8F=97=20ki;=20=E9=AA=8C=E8=AF=81(pssh=20-?= =?UTF-8?q?pwd=20=E8=BF=9E=20psshd=20-kbdint=20=E8=AE=A4=E8=AF=81=E6=88=90?= =?UTF-8?q?=E5=8A=9F);=20STATUS/AGENT/CHANGELOG=20=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENT.MD | 6 ++ CHANGELOG.md | 6 ++ STATUS.md | 2 +- apps/sshd/main.c | 4 ++ include/paze/ssh.h | 2 + src/ssh/ssh_auth.c | 135 +++++++++++++++++++++++++++++++++++++++-- src/ssh/ssh_internal.h | 5 ++ src/ssh/ssh_server.c | 4 ++ 8 files changed, 158 insertions(+), 6 deletions(-) diff --git a/AGENT.MD b/AGENT.MD index 24d00d7..236abe8 100644 --- a/AGENT.MD +++ b/AGENT.MD @@ -96,4 +96,10 @@ session ticket key 支持持久化到文件:新增 `paze_tls_config_set_session_ - `pssh ssh -o ServerAliveInterval=N`(默认15s):空闲发 `keepalive@openssh.com` global request(ssh_keepalive_send, fire-and-forget),保持空闲连接不中断;服务端回包形成保活闭环。 - `psshd` 认证失败日志对齐 OpenSSH 格式(`Invalid user`/`Failed password ... from `),并记录对端 IP(`ssh_session_set_peer_ip`),fail2ban 默认 `sshd` filter 可直接匹配封禁。 +### 9. keyboard-interactive 认证(✅ 已完成 2026-08-14) + +- 客户端:password 失败后 fallback 到 keyboard-interactive(RFC 4256,收发 INFO_REQUEST/RESPONSE,以密码响应)。 +- 服务端:方法含 `keyboard-interactive` 并复用用户库密码校验;`psshd -kbdint` 强制仅接受 ki。 +- 验证:`pssh -pwd` 连 `psshd -kbdint` 经 ki 认证成功。 + diff --git a/CHANGELOG.md b/CHANGELOG.md index 97f6afe..f2bcc36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ## [开发主线] — 2026-08-14 +### 新增 —— keyboard-interactive 认证 (RFC 4256) + +- 客户端:password 失败后 fallback 到 keyboard-interactive(发请求、收 `USERAUTH_INFO_REQUEST`、以密码响应首个 prompt)。 +- 服务端:方法列表含 `keyboard-interactive`,收发 `INFO_REQUEST/INFO_RESPONSE` 并复用用户库密码校验;`psshd -kbdint` 可强制仅接受 ki(测试/策略)。 +- 验证:`pssh -pwd` 连 `psshd -kbdint` 时经 keyboard-interactive 认证成功(exit 0,命令正常执行)。 + ### 新增 —— 长连接保活 + fail2ban 兼容 - **keepalive 保活**:`pssh ssh` 新增 `-o ServerAliveInterval=N`(默认 15s,0=禁用),空闲时定时发 `keepalive@openssh.com` global request(`ssh_keepalive_send`,fire-and-forget),保持空闲连接不被 NAT/防火墙回收;服务端对 GLOBAL_REQUEST 回包形成保活闭环。 diff --git a/STATUS.md b/STATUS.md index cb8a3ea..4f5f2cc 100644 --- a/STATUS.md +++ b/STATUS.md @@ -40,7 +40,7 @@ ## PazeSSH — SSH-2(✅ 完成) - transport:版本交换、密钥交换(curve25519-sha256)、NEWKEYS、包加密 -- 认证:password / publickey(authorized_keys 校验) +- 认证:password / **keyboard-interactive(RFC 4256,`psshd -kbdint` 仅 ki)** / publickey(authorized_keys 校验) - 通道:session(exec/shell 真实进程执行)、direct-tcpip(出站桥接) - **SFTP 子系统 v3**:OPEN/READ/WRITE/OPENDIR/READDIR/STAT/REALPATH 等,与 OpenSSH sftp / scp -s 互操作(Git OpenSSH 实测通过) - 客户端命令: diff --git a/apps/sshd/main.c b/apps/sshd/main.c index 7c2b10a..2460d74 100644 --- a/apps/sshd/main.c +++ b/apps/sshd/main.c @@ -91,6 +91,7 @@ int pssh_cmd_sshd(int argc, char **argv) { } int port = 2222; int verbose = 0; + int kbdint = 0; char users_path[1024] = ""; char ak_path[1024] = ""; for (int i = 1; i < argc; i++) { @@ -99,6 +100,8 @@ int pssh_cmd_sshd(int argc, char **argv) { snprintf(users_path, sizeof(users_path), "%s", argv[++i]); } else if (strcmp(argv[i], "-a") == 0 && i + 1 < argc) { snprintf(ak_path, sizeof(ak_path), "%s", argv[++i]); + } else if (strcmp(argv[i], "-kbdint") == 0) { + kbdint = 1; /* 仅接受 keyboard-interactive 认证 */ } else if (strcmp(argv[i], "-v") == 0) { verbose = 1; } @@ -185,6 +188,7 @@ int pssh_cmd_sshd(int argc, char **argv) { ssh_session_set_io(s, ps, tcp_read, tcp_write); ssh_session_set_users(s, users, nusers); ssh_session_set_peer_ip(s, inet_ntoa(client.sin_addr)); /* 供失败日志/fail2ban */ + if (kbdint) ssh_session_set_kbdint_only(s, 1); if (verbose) ssh_session_set_verbose(s, 1); ssh_server_loop(s, (const uint8_t *)ak_text, ak_len); diff --git a/include/paze/ssh.h b/include/paze/ssh.h index cc14c02..cae4293 100644 --- a/include/paze/ssh.h +++ b/include/paze/ssh.h @@ -34,6 +34,8 @@ void ssh_session_set_io(ssh_session_t *s, void *ctx, int (*write_fn)(void *, const uint8_t *, size_t)); /* Set peer IP(服务端 accept 后设置,用于认证失败日志的 fail2ban 兼容) */ void ssh_session_set_peer_ip(ssh_session_t *s, const char *ip); +/* 服务端仅接受 keyboard-interactive(1=启用,用于测试/策略) */ +void ssh_session_set_kbdint_only(ssh_session_t *s, int on); /* Return the underlying socket handle for event loops: * Windows: SOCKET, POSIX: int fd. -1 if none. */ diff --git a/src/ssh/ssh_auth.c b/src/ssh/ssh_auth.c index 5f9d4b9..4406be8 100644 --- a/src/ssh/ssh_auth.c +++ b/src/ssh/ssh_auth.c @@ -53,6 +53,10 @@ static int auth_service_accept(ssh_session_t *s) { } } +/* password 认证的前向声明(keyboard-interactive 亦使用) */ +static int auth_keyboard_interactive(ssh_session_t *s, + const char *username, const char *password); + /* 密码认证 */ static int auth_password(ssh_session_t *s, const char *username, const char *password) { @@ -95,12 +99,65 @@ int ssh_auth_client_loop(ssh_session_t *s, } /* Try password auth */ - if (password && *password) - return auth_password(s, username, password); + if (password && *password) { + if (auth_password(s, username, password) == 0) return 0; + /* fallback:某些服务器只提供 keyboard-interactive */ + if (auth_keyboard_interactive(s, username, password) == 0) return 0; + return -1; + } return -1; } +/* keyboard-interactive (RFC 4256):发送请求并用 password 响应首个 prompt。 + * 用于服务器不支持明文 password 而只提供 keyboard-interactive 的场景。 + * 返回 0=成功,-1=失败。 */ +static int auth_keyboard_interactive(ssh_session_t *s, + const char *username, const char *password) { + uint8_t pkt[1024]; + size_t poff = 0; + poff += ssh_str_write(pkt + poff, username); + poff += ssh_str_write(pkt + poff, "ssh-connection"); + poff += ssh_str_write(pkt + poff, "keyboard-interactive"); + poff += ssh_str_write(pkt + poff, ""); /* lang */ + poff += ssh_str_write(pkt + poff, ""); /* submethods */ + if (ssh_packet_send(s, SSH_MSG_USERAUTH_REQUEST, pkt, poff) < 0) return -1; + + for (;;) { + uint8_t type; const uint8_t *payload; size_t plen; + if (ssh_packet_recv(s, &type, &payload, &plen) < 0) return -1; + if (type == SSH_MSG_USERAUTH_SUCCESS) { free((void *)payload); return 0; } + if (type == SSH_MSG_USERAUTH_FAILURE) { free((void *)payload); return -1; } + if (type == SSH_MSG_USERAUTH_INFO_REQUEST && plen >= 8) { + /* name(4) instr(4) lang(4) n_prompts(4) ... */ + size_t off = 0; uint32_t ns, ni, nl; + uint32_t npr; + ns = paze_load32_be(payload + off); off += 4; + off += (size_t)ns; + ni = paze_load32_be(payload + off); off += 4; + off += (size_t)ni; + nl = paze_load32_be(payload + off); off += 4; + off += (size_t)nl; + npr = paze_load32_be(payload + off); off += 4; + /* 响应 password(通常首个 prompt 即密码);其余填空 */ + if (npr >= 1) { + uint8_t resp[2048]; + size_t ro = 0; + ro += ssh_uint32_write(resp + ro, npr); + ro += ssh_str_write(resp + ro, password ? password : ""); + for (uint32_t k = 1; k < npr; k++) + ro += ssh_str_write(resp + ro, ""); + if (ssh_packet_send(s, SSH_MSG_USERAUTH_INFO_RESPONSE, resp, ro) < 0) { + free((void *)payload); return -1; + } + } + free((void *)payload); + continue; + } + free((void *)payload); + } +} + /* 通过 SSH agent 完成认证:先逐个尝试 agent 身份,失败回退到密码 */ int ssh_auth_client_agent_loop(ssh_session_t *s, const char *username, @@ -126,8 +183,11 @@ int ssh_auth_client_agent_loop(ssh_session_t *s, } /* Try password auth */ - if (password && *password) - return auth_password(s, username, password); + if (password && *password) { + if (auth_password(s, username, password) == 0) return 0; + if (auth_keyboard_interactive(s, username, password) == 0) return 0; + return -1; + } return -1; } @@ -147,7 +207,8 @@ int ssh_auth_server_loop(ssh_session_t *s, const uint8_t *authorized_keys, size_t ak_len) { if (!s) return -1; int have_ak = (authorized_keys && ak_len > 0); - const char *methods = have_ak ? "publickey,password" : "password"; + const char *methods = have_ak ? "publickey,password,keyboard-interactive" : "password,keyboard-interactive"; + if (s->kbdint_only) methods = "keyboard-interactive"; /* Receive service request */ uint8_t type; const uint8_t *payload; size_t plen; @@ -189,6 +250,12 @@ int ssh_auth_server_loop(ssh_session_t *s, memcpy(method, payload + off, mn); method[mn] = '\0'; if (strcmp(method, "password") == 0) { + if (s->kbdint_only) { + /* 策略:仅接受 keyboard-interactive */ + send_auth_failure(s, methods); + free((void *)payload); + continue; + } off += mlen; /* bool + string(password) */ if (off + 5 > plen) { free((void *)payload); continue; } @@ -242,6 +309,64 @@ int ssh_auth_server_loop(ssh_session_t *s, continue; } + if (strcmp(method, "keyboard-interactive") == 0) { + /* RFC 4256:发 INFO_REQUEST(1 个 prompt),收 INFO_RESPONSE 取密码校验, + * 复用用户库密码校验(等同 password)。 */ + uint8_t ir[512]; + size_t io2 = 0; + io2 += ssh_str_write(ir + io2, ""); /* name */ + io2 += ssh_str_write(ir + io2, ""); /* instruction */ + io2 += ssh_str_write(ir + io2, ""); /* lang */ + io2 += ssh_uint32_write(ir + io2, 1); /* num_prompts */ + io2 += ssh_str_write(ir + io2, "Password"); /* prompt */ + io2 += ssh_bool_write(ir + io2, 0); /* echo */ + if (ssh_packet_send(s, SSH_MSG_USERAUTH_INFO_REQUEST, ir, io2) < 0) { + free((void *)payload); return -1; + } + /* 收 INFO_RESPONSE */ + uint8_t rtype; const uint8_t *rp; size_t rplen; + if (ssh_packet_recv(s, &rtype, &rp, &rplen) < 0) { free((void *)payload); return -1; } + uint8_t *rp2 = (uint8_t *)rp; /* 便于 off+len 计算 */ + char kb_pw[1024]; size_t kb_pwl = 0; + if (rtype == SSH_MSG_USERAUTH_INFO_RESPONSE && rplen >= 4) { + uint32_t nresp = paze_load32_be(rp2); + size_t rkoff = 4; + if (nresp >= 1 && rkoff + 4 <= rplen) { + uint32_t rl = paze_load32_be(rp2 + rkoff); rkoff += 4; + if (rkoff + rl <= rplen) { + kb_pwl = rl < sizeof(kb_pw) - 1 ? rl : sizeof(kb_pw) - 1; + memcpy(kb_pw, rp2 + rkoff, kb_pwl); kb_pw[kb_pwl] = '\0'; + } + } + } else { free((void *)payload); free((void *)rp); return -1; } + + /* 用户库查找 + 校验 */ + const ssh_user_t *u = NULL; + for (int i = 0; i < s->nusers; i++) { + size_t nl = strlen(s->users[i].name); + if (nl == ulen && memcmp(s->users[i].name, uname, ulen) == 0) { u = &s->users[i]; break; } + } + if (u && ssh_userdb_check(u, kb_pw) == 0) { + memcpy(s->username, u->name, strlen(u->name) + 1); + s->perm = u->perm; + uint8_t success = SSH_MSG_USERAUTH_SUCCESS; + ssh_packet_send(s, success, NULL, 0); + free((void *)payload); free((void *)rp); + return 0; + } + { + char unamez[256]; + size_t un = ulen < sizeof(unamez) - 1 ? ulen : sizeof(unamez) - 1; + memcpy(unamez, uname, un); unamez[un] = '\0'; + fprintf(stderr, "%s %s from %s port %u ssh2\n", + u ? "Failed password for" : "Invalid user", + unamez, s->peer_ip, (unsigned)s->port); + } + free((void *)payload); free((void *)rp); + send_auth_failure(s, methods); + continue; + } + if (strcmp(method, "publickey") == 0) { off += mlen; if (off + 1 > plen) { free((void *)payload); continue; } diff --git a/src/ssh/ssh_internal.h b/src/ssh/ssh_internal.h index aa56e38..27de0c4 100644 --- a/src/ssh/ssh_internal.h +++ b/src/ssh/ssh_internal.h @@ -60,6 +60,10 @@ #define SSH_MSG_USERAUTH_SUCCESS 52 #define SSH_MSG_USERAUTH_BANNER 53 #define SSH_MSG_USERAUTH_PK_OK 60 +/* RFC 4256 keyboard-interactive(RFC 4252 的 PK_OK 与 INFO_REQUEST 同为 60, + * 按认证流程上下文区分;INFO_RESPONSE 为 61) */ +#define SSH_MSG_USERAUTH_INFO_REQUEST 60 +#define SSH_MSG_USERAUTH_INFO_RESPONSE 61 /* Connection */ #define SSH_MSG_GLOBAL_REQUEST 80 @@ -283,6 +287,7 @@ typedef struct { char username[256]; char hostname[256]; char peer_ip[64]; /* 对端 IP(服务端 accept 时设置,供日志/fail2ban) */ + int kbdint_only; /* 服务端仅接受 keyboard-interactive(测试/策略用) */ uint16_t port; int is_server; diff --git a/src/ssh/ssh_server.c b/src/ssh/ssh_server.c index 2ce4e54..b897d4a 100644 --- a/src/ssh/ssh_server.c +++ b/src/ssh/ssh_server.c @@ -25,6 +25,10 @@ void ssh_session_set_peer_ip(ssh_session_t *s, const char *ip) { s->peer_ip[n] = '\0'; } +void ssh_session_set_kbdint_only(ssh_session_t *s, int on) { + if (s) s->kbdint_only = on ? 1 : 0; +} + #ifdef _WIN32 #include #endif