GitHub - 使用SSH进行连接(续)

news2024/12/26 10:47:10

文章目录

  • 前言
  • 开发环境
  • SSH源码获取
  • SSH源码分析
  • 最后


前言

上篇文章中提出了存在一些默认密钥文件会被SSH自动添加的猜测,现在我们通过一些分析来验证这个猜测。

开发环境

  • MacOS: 14.3.1
  • SSH: OpenSSH_9.4p1

SSH源码获取

该怎么验证这个猜测呢?有个方法简单又可靠:翻源码!先执行ssh -V命令看看当前SSH的版本什么:

OpenSSH_9.4p1, LibreSSL 3.3.6

从版本信息可知,用的是OpenSSH的便携式版本。从GitHub克隆项目到本地(镜像很多,按需选择):

git clone https://github.com/openssh/openssh-portable.git

SSH源码分析

将已知的默认名称id_rsa作为关键词搜索项目,可以找到pathnames.h文件:

/*
 * The directory in user's home directory in which the files reside. The
 * directory should be world-readable (though not all files are).
 */
#define _PATH_SSH_USER_DIR		".ssh"

...

/*
 * Name of the default file containing client-side authentication key. This
 * file should only be readable by the user him/herself.
 */
#define _PATH_SSH_CLIENT_ID_DSA		_PATH_SSH_USER_DIR "/id_dsa"
#define _PATH_SSH_CLIENT_ID_ECDSA	_PATH_SSH_USER_DIR "/id_ecdsa"
#define _PATH_SSH_CLIENT_ID_RSA		_PATH_SSH_USER_DIR "/id_rsa"
#define _PATH_SSH_CLIENT_ID_ED25519	_PATH_SSH_USER_DIR "/id_ed25519"
#define _PATH_SSH_CLIENT_ID_XMSS	_PATH_SSH_USER_DIR "/id_xmss"
#define _PATH_SSH_CLIENT_ID_ECDSA_SK	_PATH_SSH_USER_DIR "/id_ecdsa_sk"
#define _PATH_SSH_CLIENT_ID_ED25519_SK	_PATH_SSH_USER_DIR "/id_ed25519_sk"

/*
 * Configuration file in user's home directory.  This file need not be
 * readable by anyone but the user him/herself, but does not contain anything
 * particularly secret.  If the user's home directory resides on an NFS
 * volume where root is mapped to nobody, this may need to be world-readable.
 */
#define _PATH_SSH_USER_CONFFILE		_PATH_SSH_USER_DIR "/config"

结合源码注释和后续分析可知,_PATH_SSH_USER_DIR即默认目录(由后续fill_default_options函数中该常量的使用可知默认路径指的是~/.ssh),_PATH_SSH_CLIENT_ID_xxx即默认名称(拼接了默认目录),_PATH_SSH_USER_CONFFILE即默认配置文件。

关于id_xmss,上一片文章中没有写上是因为ssh-keygen -t创建时并不支持xmss参数(可以通过man ssh-keygen命令查看)。个人猜测是由于XMSS算法比较新还未广泛普及验证,所以OpenSSH还没有正式支持。

继续将_PATH_SSH_CLIENT_ID_RSA作为关键词继续在项目中搜索,可以在搜索结果中找到readconf.c文件,打开找到相关源码:

void
add_identity_file(Options *options, const char *dir, const char *filename,
    int userprovided)
{
	char *path;
	int i;

    // SSH_MAX_IDENTITY_FILES = 100 (定义于ssh.h文件),密钥文件个数不能超过100
	if (options->num_identity_files >= SSH_MAX_IDENTITY_FILES)
		fatal("Too many identity files specified (max %d)",
		    SSH_MAX_IDENTITY_FILES);

    // 添加用户提供的密钥文件时,文件名就是绝对路径(由后续分析可知,config配置文件中的IdentityFile值会作为filename传入,而dir是NULL)
	if (dir == NULL) /* no dir, filename is absolute */
		path = xstrdup(filename);
	else if (xasprintf(&path, "%s%s", dir, filename) >= PATH_MAX)
		fatal("Identity file path %s too long", path);

    // 避免重复添加密钥文件,通过比较密钥文件来源和路径是否一致判断是否重复
	/* Avoid registering duplicates */
	for (i = 0; i < options->num_identity_files; i++) {
		if (options->identity_file_userprovided[i] == userprovided &&
		    strcmp(options->identity_files[i], path) == 0) {
			debug2_f("ignoring duplicate key %s", path);
			free(path);
			return;
		}
	}

    // 保存密钥文件路径
    // 补充一点,当前函数并没有检查密钥文件路径是否有效,只有在使用时出现问题才会报错
	options->identity_file_userprovided[options->num_identity_files] =
	    userprovided;
	options->identity_files[options->num_identity_files++] = path;
}

...

/*
 * Called after processing other sources of option data, this fills those
 * options for which no value has been specified with their default values.
 */
int
fill_default_options(Options * options)
{
	...
	if (options->num_identity_files == 0) {
		add_identity_file(options, "~/", _PATH_SSH_CLIENT_ID_RSA, 0);
#ifdef OPENSSL_HAS_ECC
		add_identity_file(options, "~/", _PATH_SSH_CLIENT_ID_ECDSA, 0);
		add_identity_file(options, "~/",
		    _PATH_SSH_CLIENT_ID_ECDSA_SK, 0);
#endif
		add_identity_file(options, "~/",
		    _PATH_SSH_CLIENT_ID_ED25519, 0);
		add_identity_file(options, "~/",
		    _PATH_SSH_CLIENT_ID_ED25519_SK, 0);
		add_identity_file(options, "~/", _PATH_SSH_CLIENT_ID_XMSS, 0);
#ifdef WITH_DSA
		add_identity_file(options, "~/", _PATH_SSH_CLIENT_ID_DSA, 0);
#endif
	}
	...
}

顾名思义,fill_default_options函数用于填充默认值,当满足条件num_identity_files == 0时添加默认密钥文件,同时也通过userprovided = 0标记该密钥不是用户提供的(来源系统默认)。

add_identity_file函数最后保存密钥文件时,对num_identity_files进行了递增操作(++)。如果SSHfill_default_options函数调用前先添加config配置文件中指定的密钥文件,这时应该会调用add_identity_file函数进而导致num_identity_files == 0条件不满足,不再自动添加默认密钥文件。真的会这样吗?

fill_default_options作为关键词在项目中搜索,可以在ssh.c文件找到相关源码:

/*
 * Read per-user configuration file.  Ignore the system wide config
 * file if the user specifies a config file on the command line.
 */
static void
process_config_files(const char *host_name, struct passwd *pw, int final_pass,
    int *want_final_pass)
{
	char buf[PATH_MAX];
	int r;
    
	if (config != NULL) {
		if (strcasecmp(config, "none") != 0 &&
		    !read_config_file(config, pw, host, host_name, &options,
		    SSHCONF_USERCONF | (final_pass ? SSHCONF_FINAL : 0),
		    want_final_pass))
			fatal("Can't open user config file %.100s: "
			    "%.100s", config, strerror(errno));
	} else {
        // _PATH_SSH_USER_CONFFILE即.ssh/config,定义于pathnames.h文件
		r = snprintf(buf, sizeof buf, "%s/%s", pw->pw_dir,
		    _PATH_SSH_USER_CONFFILE);
		if (r > 0 && (size_t)r < sizeof(buf))
			(void)read_config_file(buf, pw, host, host_name,
			    &options, SSHCONF_CHECKPERM | SSHCONF_USERCONF |
			    (final_pass ? SSHCONF_FINAL : 0), want_final_pass);

		/* Read systemwide configuration file after user config. */
		(void)read_config_file(_PATH_HOST_CONFIG_FILE, pw,
		    host, host_name, &options,
		    final_pass ? SSHCONF_FINAL : 0, want_final_pass);
	}
}

...
    
/*
 * Main program for the ssh client.
 */
int
main(int ac, char **av)
{
    ...
    /* Parse the configuration files */
    process_config_files(options.host_arg, pw, 0, &want_final_pass);
    if (want_final_pass)
        debug("configuration requests final Match pass");
	...
	/* Fill configuration defaults. */
	if (fill_default_options(&options) != 0)
		cleanup_exit(255);
	...
}

fill_default_options函数的调用位置位于SSH的入口函数(main),这无疑验证了前面关于SSH会自动添加默认密钥的猜测。 同时,在fill_default_options函数调用前会先解析配置文件。不过,还不确定配置文件是否会导致不再自动添加默认密钥文件。

继续往下分析,加载及解析配置文件的相关函数在readconf.c文件,函数的大致调用顺序:read_config_file -> read_config_file_depth -> process_config_line_depth。其中的逻辑阅读不难,本质是逐行解析,着重看看process_config_line_depth函数:

static struct {
	const char *name;
	OpCodes opcode;
} keywords[] = {
	...
	{ "identityfile", oIdentityFile },
    ...
    { "host", oHost },
    ...
};

/*
 * Returns the number of the token pointed to by cp or oBadOption.
 */
static OpCodes
parse_token(const char *cp, const char *filename, int linenum,
    const char *ignored_unknown)
{
	int i;
    // keywords数组存储着关键字(例如identityfile)和操作码(例如oIdentityFile)的映射关系
    // 通过遍历数组匹配关键字并返回对应操作码
	for (i = 0; keywords[i].name; i++)
		if (strcmp(cp, keywords[i].name) == 0)
			return keywords[i].opcode;
	if (ignored_unknown != NULL &&
	    match_pattern_list(cp, ignored_unknown, 1) == 1)
		return oIgnoredUnknownOption;
	error("%s: line %d: Bad configuration option: %s",
	    filename, linenum, cp);
	return oBadOption;
}

...

static int
process_config_line_depth(Options *options, struct passwd *pw, const char *host,
    const char *original_host, char *line, const char *filename,
    int linenum, int *activep, int flags, int *want_final_pass, int depth)
{
    ...
    // 将关键字转为小写,这是因为在keywords数组中关键字全是小写的
    // 由此可见config文件中的关键字并不区分大小写,在这之前我还以为config文件中的关键字必须要用大驼峰命名(建议还是继续使用大驼峰命名,可读性高)
    /* Match lowercase keyword */
    lowercase(keyword);
	...
    // 将关键字转为操作码
	opcode = parse_token(keyword, filename, linenum,
	    options->ignored_unknown);
	...
	switch (opcode) {
    ...
	case oIdentityFile:
		arg = argv_next(&ac, &av);
		if (!arg || *arg == '\0') {
			error("%.200s line %d: Missing argument.",
			    filename, linenum);
			goto out;
		}
		if (*activep) {
			intptr = &options->num_identity_files;
			if (*intptr >= SSH_MAX_IDENTITY_FILES) {
				error("%.200s line %d: Too many identity files "
				    "specified (max %d).", filename, linenum,
				    SSH_MAX_IDENTITY_FILES);
				goto out;
			}
			add_identity_file(options, NULL,
			    arg, flags & SSHCONF_USERCONF);
		}
		break;
    ...
	case oHost:
		if (cmdline) {
			error("Host directive not supported as a command-line "
			    "option");
			goto out;
		}
		*activep = 0;
		arg2 = NULL;
		while ((arg = argv_next(&ac, &av)) != NULL) {
			if (*arg == '\0') {
				error("%s line %d: keyword %s empty argument",
				    filename, linenum, keyword);
				goto out;
			}
			if ((flags & SSHCONF_NEVERMATCH) != 0) {
				argv_consume(&ac);
				break;
			}
			negated = *arg == '!';
			if (negated)
				arg++;
			if (match_pattern(host, arg)) {
				if (negated) {
					debug("%.200s line %d: Skipping Host "
					    "block because of negated match "
					    "for %.100s", filename, linenum,
					    arg);
					*activep = 0;
					argv_consume(&ac);
					break;
				}
				if (!*activep)
					arg2 = arg; /* logged below */
				*activep = 1;
			}
		}
		if (*activep)
			debug("%.200s line %d: Applying options for %.100s",
			    filename, linenum, arg2);
		break;
    ...
}

从处理oIdentityFile操作码的逻辑可知,当*activep等于1时,将会调用add_identity_file函数添加配置文件中指定的密钥文件,进而导致num_identity_files == 0条件不满足,后续不会再自动添加默认密钥文件。

所以,如果config配置文件中指定了的密钥文件,确实会导致不再自动添加默认密钥文件。

这里还有一个疑问,*activep什么时候会等于1?请看处理oHost操作码的逻辑,只有当Host匹配上时*activep才会等于1。举个简单的例子🌰:

你使用SSH连接Hostgithub.com的服务器,然后你在config文件的配置如下:

Host github.com
  IdentityFile ~/.ssh/id_ed25519.github

解析配置文件时,Host就匹配上了。你用ssh -G github.com命令(ssh -G命令用于打印SSH客户端的配置信息)就会看到输出中和IdentityFile相关的只有你配置的:

screenshot1

此时默认密钥文件并没有自动添加(具体原因前面已分析)。如果你用ssh -G github.cn命令,由于需要连接的Hostgithub.cn,与配置文件中的不匹配,你会在输出中看到一堆自动添加的默认密钥文件:

screenshot2

综上,确实存在一些默认密钥文件会被SSH自动添加。不过,当SSH连接的Hostconfig文件有配置IdentityFile时,将不会自动添加这些默认密钥文件。

最后

如果这篇文章对你有所帮助,点赞👍收藏🌟支持一下吧,谢谢~


本篇文章由@crasowas发布于CSDN。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1559633.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

QT初识(2)

QT初识&#xff08;2&#xff09; 创建好项目之后&#xff0c;多了些什么东西&#xff1f;main.cppwidget.hwidget.cppwidget.ui.pro项目工程文件 我们今天来继续了解QT。如果没看过上一次QT初识的小伙伴可以点击这里&#xff1a; https://blog.csdn.net/qq_67693066/article/d…

ADB(Android Debug Bridge)操作命令详解及示例

ADB&#xff08;Android Debug Bridge&#xff09;是一个强大的命令行工具&#xff0c;它是Android SDK的一部分&#xff0c;主要用于Android设备&#xff08;包括真实手机和平板电脑以及模拟器&#xff09;的调试、系统控制和应用程序部署。 下面是一些ADB的常用命令&#xff…

全国植被覆盖度VFC逐月数据/NDVI/净初级生产力NPP/植被类型​

引言 植被覆盖度是指植被&#xff08;包括叶、茎、枝&#xff09;在地面的垂直投影面积占统计区总面积的百分比。是刻画地表植被覆盖的一个重要参数, 也是指示生态环境变化的重要指标之一。 正文 数据简介 容易与植被覆盖度混淆的概念是植被盖度&#xff0c;植被盖度是指植被冠…

腾讯云轻量服务器8核16G服务器价格1668元一年送3个月,18M大带宽

腾讯云轻量应用服务器8核16G配置租用优惠价格1668元15个月&#xff0c;买一年送3个月&#xff0c;配置为&#xff1a;轻量8核16G18M、270GB SSD盘、3500GB月流量、18M带宽&#xff0c;腾讯云优惠活动 yunfuwuqiba.com/go/txy 活动链接打开如下图&#xff1a; 腾讯云8核16G服务器…

复写零->C语言和JAVA版本的双指针解法

使用C语言和JAVA语言版本双指针来解决复写零问题 力扣链接:https://leetcode.cn/problems/duplicate-zeros/description/ 题意:对一个数组进行复写,遇到0写两遍,非0写一遍,复写结果不能超过原数组长度,即当复写结果达到数组长度时,后面的结果数组元素直接舍弃. 例子 思路:找到…

Spring Boot单元测试全指南:使用Mockito和AssertJ

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

机器学习每周挑战——旅游景点数据分析

数据的截图&#xff0c;数据的说明&#xff1a; # 字段 数据类型 # 城市 string # 名称 string # 星级 string # 评分 float # 价格 float # 销量 int # 省/市/区 string # 坐标 string # 简介 string # 是否免费 bool # 具体地址 string拿到数据…

SAP 学习笔记 - 系统移行业务 - Migration cockpit工具 - 移行Material(品目)

本章开始&#xff0c;来研究研究移行工具 Migration cockpit。 理论啥的先放一边&#xff0c;来先做一个简单的实例&#xff0c;以对 Migration cockpit 有个大概的印象。 这里就先做一个移行品目的例子。 1&#xff0c;LTMC 启动Migration cockpit工具 默认给我启动了 IE &a…

Python基础之pandas:Series和DataFrame定义及使用

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、Series特点二、Series使用步骤1.Series定义2.索引和切片3.series的.get()4.掩码提取5.Series运算符和广播方法6.ufunc在Series对象中使用 三、DataFrame1.D…

使用python实现i茅台自动预约

使用python实现i茅台自动预约[仅限于学习&#xff0c;不可商用] 运行&#xff1a; 直接运行 imtApi.py 打包&#xff1a;切换到imt脚本目录&#xff0c;执行打包命令&#xff1a; pyinstaller --onefile imtApi.py这个应用程序可以帮助你进行茅台自动化配置。以下是一些使用…

【Laravel】06 数据库迁移工具migration

【Laravel】06 数据库迁移工具migration 1.migration文件目录2. 举例 1.migration文件目录 2. 举例 (base) ➜ example-app php artisan migrate Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_crea…

Java基础学习: JDK动态代理

文章目录 一、什么是JDK动态代理二、JDK动态代理的特点三、JDK动态代理类如何使用四、JDK动态代理原理分析1、创建代理对象2、生成代理类 一、什么是JDK动态代理 JDK动态代理是Java提供的一种动态生成代理类和代理对象的技术。它主要利用Java的反射机制实现&#xff0c;在运行…

算法学习——LeetCode力扣动态规划篇10(583. 两个字符串的删除操作、72. 编辑距离、647. 回文子串、516. 最长回文子序列)

算法学习——LeetCode力扣动态规划篇10 583. 两个字符串的删除操作 583. 两个字符串的删除操作 - 力扣&#xff08;LeetCode&#xff09; 描述 给定两个单词 word1 和 word2 &#xff0c;返回使得 word1 和 word2 相同所需的最小步数。 每步 可以删除任意一个字符串中的一个…

2010-2021年各省碳排放测算数据(含原始数据+计算过程+结果)

2010-2021年各省碳排放测算数据&#xff08;含原始数据计算过程结果&#xff09; 1、时间&#xff1a;2010-2021年 2、指标&#xff1a;原煤(万吨)、原煤(万吨CO2)、焦炭(万吨)、焦炭(万吨CO2)、汽油(万吨)、汽油(万吨CO2)、煤油(万吨)、煤油(万吨CO2)、柴油(万吨)、柴油(万吨…

vivado XVC 服务器实现

XVC 服务器实现 您需要实现 XVC 协议才能在相应的处理器上创建 XVC 服务器。 XVC 协议 XVC 协议允许 Vivado IDE 通过以太网向嵌入式系统发送 JTAG 命令 &#xff0c; 以便对目标赛灵思器件进行编程和 / 或调试。这样 即可采用任意供应商解决方案来对赛灵思器件进行调…

《Java面试自救指南》(专题一)操作系统

文章目录 力推操作系统的三门神课操作系统的作用和功能线程、进程和协程的区别并行与并发的区别什么是文件描述符操作系统内核态和用户态的区别用户态切换到内核态的方式大内核和微内核的区别用户级线程和内核级线程的区别线程的七态模型进程调度算法有哪些进程间通信的七种方式…

Python之Opencv进阶教程(2):统计图片灰度级别的像素数量

1、什么是灰度像素数量 在OpenCV中&#xff0c;可以使用**cv2.calcHist()**函数来计算图像的直方图。直方图是一种图形统计表&#xff0c;用于表示图像中每个灰度级别&#xff08;或颜色通道&#xff09;的像素数量或密度分布。以下是一个示例代码&#xff0c;演示了如何使用O…

Qt源程序编译及错误问题解决

Error 5 while parsing C:/qt-everywhere-src-6.6.2/qt-build/qtdeclarative/src/qmlmodels/meta_types/qt6qmlmodels_release_metatypes.json: illegal value .json 文件为空文件0字节&#xff0c;加 “[]”&#xff0c;不要引号。可以解决这类错误。 Qt编译 Qt for Windows…

重读Java设计模式: 深入探讨建造者模式,构建复杂对象的优雅解决方案

引言 在软件开发中&#xff0c;有时需要构建具有复杂结构的对象&#xff0c;如果直接使用构造函数或者 setter 方法逐个设置对象的属性&#xff0c;会导致代码变得冗长、难以维护&#xff0c;并且容易出错。为了解决这个问题&#xff0c;我们可以使用建造者模式。 一、建造者…

CCF2025上海国际日用百货(春季)博览会

CCF2025上海国际日用百货&#xff08;春季&#xff09;博览会 时间&#xff1a;2025年3月7-9日 地点&#xff1a;上海新国际博览中心 预订以上展会详询陆先生 I38&#xff08;前三位&#xff09; I82I&#xff08;中间四位&#xff09; 9I72&#xff08;后面四位&#xf…