TLS 1.3 PSK 会话恢复: NewSessionTicket 签发/解析、ticket+binder 校验、selected_identity 回选、恢复握手免证书(pazessl -sess_in/-sess_out 端到端验证); 构建脚本输出 bin/ 并支持独立命令与 psftp; 修复 SFTP 二进制传输与 copy_id -P; 补充 tests/ 调试与 verify_tls
256 行
8.4 KiB
C
256 行
8.4 KiB
C
/* pssh copy-id —— 安装公钥到远程主机 (平替 ssh-copy-id)
|
|
*
|
|
* 用法:
|
|
* paze-copy-id [-i keyfile] [-p port] [-pwd password] [user@]host
|
|
*
|
|
* 将公钥追加到远程 ~/.ssh/authorized_keys。
|
|
* -i 可指定 .pub 公钥文件或私钥文件(未指定时尝试 ~/.ssh/id_*.pub)。
|
|
* 认证优先使用匹配的私钥,否则使用 -pwd 提供的密码(或交互输入)。
|
|
*/
|
|
#include "paze/ssh.h"
|
|
#include "paze/ssh_keys.h"
|
|
#include "paze/encoding.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <conio.h>
|
|
#define GETCH _getch
|
|
#else
|
|
#include <termios.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
static void sshdir_path(char *buf, size_t sz, const char *rel) {
|
|
const char *home = NULL;
|
|
#ifdef _WIN32
|
|
home = getenv("USERPROFILE");
|
|
if (!home) home = getenv("HOME");
|
|
#else
|
|
home = getenv("HOME");
|
|
#endif
|
|
if (!home) home = ".";
|
|
snprintf(buf, sz, "%s/.ssh/%s", home, rel);
|
|
}
|
|
|
|
static uint8_t *read_file(const char *path, size_t *len) {
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) return NULL;
|
|
fseek(f, 0, SEEK_END);
|
|
long sz = ftell(f);
|
|
rewind(f);
|
|
if (sz <= 0 || sz > 4 * 1024 * 1024) { fclose(f); return NULL; }
|
|
uint8_t *buf = (uint8_t *)malloc((size_t)sz);
|
|
if (!buf) { fclose(f); return NULL; }
|
|
if (fread(buf, 1, (size_t)sz, f) != (size_t)sz) {
|
|
free(buf); fclose(f); return NULL;
|
|
}
|
|
fclose(f);
|
|
*len = (size_t)sz;
|
|
return buf;
|
|
}
|
|
|
|
static void read_password(const char *prompt, char *out, size_t outsz) {
|
|
fprintf(stderr, "%s", prompt);
|
|
int idx = 0, ch;
|
|
while (idx < (int)outsz - 1) {
|
|
ch = GETCH();
|
|
if (ch == '\r' || ch == '\n') break;
|
|
if (ch == '\b' || ch == 0x7f) {
|
|
if (idx > 0) { idx--; fprintf(stderr, "\b \b"); }
|
|
continue;
|
|
}
|
|
if (ch < 32) continue;
|
|
out[idx++] = (char)ch;
|
|
fprintf(stderr, "*");
|
|
}
|
|
out[idx] = '\0';
|
|
fprintf(stderr, "\n");
|
|
}
|
|
|
|
/* 执行远程命令并打印输出 */
|
|
static int exec_and_drain(ssh_session_t *s, const char *cmd) {
|
|
uint32_t ch = 0;
|
|
if (ssh_channel_open(s, &ch, "session", 0, 0) < 0) {
|
|
fprintf(stderr, "paze-copy-id: channel open failed\n");
|
|
return -1;
|
|
}
|
|
if (ssh_channel_request_exec(s, ch, cmd) < 0) {
|
|
fprintf(stderr, "paze-copy-id: exec failed\n");
|
|
return -1;
|
|
}
|
|
uint8_t buf[8192];
|
|
for (;;) {
|
|
uint32_t rid = 0;
|
|
size_t n = sizeof(buf);
|
|
int r = ssh_channel_recv_data(s, &rid, buf, &n);
|
|
if (r < 0) break; /* EOF / close */
|
|
if (n > 0 && n < sizeof(buf)) {
|
|
fwrite(buf, 1, n, stdout);
|
|
fflush(stdout);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int pssh_cmd_copyid(int argc, char **argv) {
|
|
const char *keyfile = NULL;
|
|
const char *password_arg = NULL;
|
|
const char *host = NULL;
|
|
const char *user = NULL;
|
|
int port = 22;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
if ((strcmp(argv[i], "-i") == 0) && i + 1 < argc) keyfile = argv[++i];
|
|
else if ((strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "-P") == 0) && i + 1 < argc) port = atoi(argv[++i]);
|
|
else if (strcmp(argv[i], "-pwd") == 0 && i + 1 < argc) password_arg = argv[++i];
|
|
else if (argv[i][0] == '-') continue;
|
|
else {
|
|
char *at = strchr(argv[i], '@');
|
|
if (at) {
|
|
*at = '\0';
|
|
user = argv[i];
|
|
host = at + 1;
|
|
} else {
|
|
host = argv[i];
|
|
}
|
|
}
|
|
}
|
|
if (!host) {
|
|
fprintf(stderr, "Usage: paze-copy-id [-i keyfile] [-p/-P port] [-pwd password] [user@]host\n");
|
|
return 1;
|
|
}
|
|
if (!user) {
|
|
const char *envu = getenv("USER");
|
|
if (!envu) envu = getenv("USERNAME");
|
|
user = envu ? envu : "root";
|
|
}
|
|
|
|
/* 确定公钥行 + 认证私钥文件 */
|
|
char keyline[8192] = {0};
|
|
char priv_path[1100] = {0};
|
|
const char *privfile = NULL;
|
|
|
|
if (!keyfile) {
|
|
/* 尝试默认公钥 ~/.ssh/id_*.pub */
|
|
static const char *names[] = { "id_ed25519.pub", "id_rsa.pub", "id_ecdsa.pub" };
|
|
for (int i = 0; i < 3; i++) {
|
|
char p[1100];
|
|
sshdir_path(p, sizeof(p), names[i]);
|
|
size_t len = 0;
|
|
uint8_t *d = read_file(p, &len);
|
|
if (d) {
|
|
memcpy(keyline, d, len > sizeof(keyline) - 1 ? sizeof(keyline) - 1 : len);
|
|
free(d);
|
|
size_t nl = strlen(keyline);
|
|
while (nl > 0 && (keyline[nl-1] == '\n' || keyline[nl-1] == '\r')) keyline[--nl] = '\0';
|
|
snprintf(priv_path, sizeof(priv_path), "%s", p);
|
|
size_t pl = strlen(priv_path);
|
|
if (pl > 4) priv_path[pl - 4] = '\0'; /* 去掉 .pub */
|
|
privfile = priv_path;
|
|
break;
|
|
}
|
|
}
|
|
if (!keyline[0]) {
|
|
fprintf(stderr, "paze-copy-id: no public key found in ~/.ssh (use -i)\n");
|
|
return 1;
|
|
}
|
|
} else {
|
|
size_t len = 0;
|
|
uint8_t *d = read_file(keyfile, &len);
|
|
if (!d) {
|
|
fprintf(stderr, "paze-copy-id: cannot read %s\n", keyfile);
|
|
return 1;
|
|
}
|
|
int is_priv = (len > 20 &&
|
|
strstr((const char *)d, "PRIVATE KEY") != NULL);
|
|
if (is_priv) {
|
|
/* 私钥文件:解析并生成公钥行 */
|
|
ssh_privkey_t *k = NULL;
|
|
if (ssh_privkey_parse(d, len, &k) != 0) {
|
|
fprintf(stderr, "paze-copy-id: cannot parse private key %s\n", keyfile);
|
|
free(d);
|
|
return 1;
|
|
}
|
|
size_t blen = 0;
|
|
const uint8_t *blob = ssh_privkey_pubblob(k, &blen);
|
|
char b64[8192];
|
|
paze_base64_encode(blob, blen, b64);
|
|
const char *alg = ssh_privkey_alg(k);
|
|
size_t alglen = strlen(alg), b64len = strlen(b64);
|
|
if (alglen + 1 + b64len + 1 > sizeof(keyline)) {
|
|
fprintf(stderr, "paze-copy-id: key line too long\n");
|
|
free(d);
|
|
ssh_privkey_free(k);
|
|
return 1;
|
|
}
|
|
memcpy(keyline, alg, alglen);
|
|
keyline[alglen] = ' ';
|
|
memcpy(keyline + alglen + 1, b64, b64len);
|
|
keyline[alglen + 1 + b64len] = '\0';
|
|
ssh_privkey_free(k);
|
|
privfile = keyfile;
|
|
} else {
|
|
memcpy(keyline, d, len > sizeof(keyline) - 1 ? sizeof(keyline) - 1 : len);
|
|
size_t nl = strlen(keyline);
|
|
while (nl > 0 && (keyline[nl-1] == '\n' || keyline[nl-1] == '\r')) keyline[--nl] = '\0';
|
|
/* 尝试同目录私钥(去掉 .pub) */
|
|
snprintf(priv_path, sizeof(priv_path), "%s", keyfile);
|
|
size_t pl = strlen(priv_path);
|
|
if (pl > 4 && strcmp(priv_path + pl - 4, ".pub") == 0) {
|
|
priv_path[pl - 4] = '\0';
|
|
privfile = priv_path;
|
|
}
|
|
}
|
|
free(d);
|
|
}
|
|
|
|
/* 认证私钥(读入内存)或密码 */
|
|
uint8_t *keydata = NULL; size_t keylen = 0;
|
|
if (privfile) {
|
|
keydata = read_file(privfile, &keylen);
|
|
if (!keydata)
|
|
fprintf(stderr, "paze-copy-id: warning: cannot read private key %s\n", privfile);
|
|
}
|
|
char pw_buf[256];
|
|
const char *password = password_arg;
|
|
if (!password && !keydata) {
|
|
read_password("Password: ", pw_buf, sizeof(pw_buf));
|
|
password = pw_buf;
|
|
}
|
|
|
|
/* 连接并认证 */
|
|
fprintf(stderr, "paze-copy-id: connecting to %s@%s:%d\n", user, host, port);
|
|
ssh_session_t *s = ssh_session_new(0);
|
|
if (!s) { free(keydata); return 1; }
|
|
ssh_session_set_hostkey_check(s, 2);
|
|
|
|
if (ssh_client_connect(s, host, (uint16_t)port) < 0) {
|
|
fprintf(stderr, "paze-copy-id: connection failed\n");
|
|
free(keydata);
|
|
ssh_session_free(s);
|
|
return 1;
|
|
}
|
|
if (ssh_auth_client_loop(s, user, password, keydata, keylen) < 0) {
|
|
fprintf(stderr, "paze-copy-id: authentication failed\n");
|
|
free(keydata);
|
|
ssh_session_free(s);
|
|
return 1;
|
|
}
|
|
free(keydata);
|
|
|
|
/* 追加公钥 */
|
|
char cmd[12000];
|
|
snprintf(cmd, sizeof(cmd),
|
|
"umask 077; mkdir -p ~/.ssh && printf '%%s\\n' '%s' >> ~/.ssh/authorized_keys",
|
|
keyline);
|
|
int rc = exec_and_drain(s, cmd);
|
|
if (rc == 0)
|
|
fprintf(stderr, "paze-copy-id: key installed to %s@%s:~/.ssh/authorized_keys\n",
|
|
user, host);
|
|
|
|
ssh_session_free(s);
|
|
return rc;
|
|
}
|