PostgreSQL源码分析——口令认证

news2024/11/10 14:31:59
认证机制

对于数据库系统来说,其作为服务端,接受来自客户端的请求。对此,必须有对客户端的认证机制,只有通过身份认证的客户端才可以访问数据库资源,防止非法用户连接数据库。PostgreSQL支持认证方法有很多:

typedef enum UserAuth
{
	uaReject,
	uaImplicitReject,			/* Not a user-visible option */
	uaTrust,
	uaIdent,
	uaPassword,
	uaMD5,
	uaSCRAM,
	uaGSS,
	uaSSPI,
	uaPAM,
	uaBSD,
	uaLDAP,
	uaCert,
	uaRADIUS,
	uaPeer
#define USER_AUTH_LAST uaPeer	/* Must be last value of this enum */
} UserAuth;

对此,我们仅分析常用的口令认证方式。目前PostgreSQL支持MD5、SCRAM-SHA-256对密码哈希和身份验证支持,建议使用SCRAM-SHA-256,相较MD5安全性更高。不同的基于口令的认证方法的可用性取决于用户的口令在服务器上是如何被加密(或者更准确地说是哈希)的。这由设置口令时的配置参数password_encryption控制。可用值为scham-sha-256md5

host    all             all             0.0.0.0/0               scham-sha-256

口令认证并不是简单的客户端告诉服务端密码,服务端比对成功就可以了。而是要设计一个客户端和服务器协商身份验证机制,避免密码明文存储等等问题。对此,PostgreSQL引入SCRAM身份验证机制(RFC5802、RFC7677)

image.png

也就是说,后面的口令认证方法中,遵循的是如上的口令认证协议。

参考文档:53.3.1. SCRAM-SHA-256认证

源码分析

这里只分析口令认证的情况。 口令认证分为明文口令认证和加密口令认证。明文口令认证要求客户端提供一个未加密的口令进行认证,安全性较差,已经被禁止使用。加密口令认证要求客户端提供一个经过SCRAM-SHA-256加密的口令进行认证,该口令在传送过程中使用了结合salt的单向哈希加密,增强了安全性。口令都存储在pg_authid系统表中,可通过CREATE USER test WITH PASSWORD '******';等命令进行设置。

客户端

以psql客户端为例,当客户端发出连接请求后,分析口令认证的过程。

main
--> parse_psql_options(argc, argv, &options);
    // 连接数据库,中间会有认证这块,有不同的认证方法,这里列的是口令认证方式的流程
--> do 
    {
         // 输入host、port、user、password、dbname等信息
         PQconnectdbParams(keywords, values, true);
         --> PQconnectStartParams(keywords, values, expand_dbname);
             --> makeEmptyPGconn();
             --> conninfo_array_parse(keywords, values, &conn->errorMessage, true, expand_dbname);
             --> fillPGconn(conn, connOptions)
             --> connectDBStart(conn)
                 --> PQconnectPoll(conn)    // 尝试建立TCP连接
                     --> socket(addr_cur->ai_family, SOCK_STREAM, 0);
                     --> connect(conn->sock, addr_cur->ai_addr, addr_cur->ai_addrlen)
         --> connectDBComplete(conn);
             // 认证这块与数据库服务端交互以及状态很多,这里没有全部列出。
             --> PQconnectPoll(conn);
                 --> pqReadData(conn);
                     --> pqsecure_read(conn, conn->inBuffer + conn->inEnd, conn->inBufSize - conn->inEnd);
                         --> pqsecure_raw_read(conn, ptr, len);
                             --> recv(conn->sock, ptr, len, 0);
                 --> pg_fe_sendauth(areq, msgLength, conn);
                     --> pg_SASL_init(conn, payloadlen)
                         --> conn->sasl->init(conn,password,selected_mechanism);
                             --> pg_saslprep(password, &prep_password);
                         --> conn->sasl->exchange(conn->sasl_state, NULL, -1, &initialresponse, &initialresponselen, &done, &success);
                             --> build_client_first_message(state);
                                 --> pg_strong_random(raw_nonce, SCRAM_RAW_NONCE_LEN)
         PQconnectionNeedsPassword(pset.db)     // 是否需要密码,如果需要,等待用户数据密码

    }
    // 进入主循环,等待用户输入命令,执行命令
--> MainLoop(stdin);  
    --> psql_scan_create(&psqlscan_callbacks);
    --> while (successResult == EXIT_SUCCESS)
        {
            // 等待用户输入命令
            gets_interactive(get_prompt(prompt_status, cond_stack), query_buf);

            SendQuery(query_buf->data); // 把用户输入的SQL命令发送到数据库服务
            --> ExecQueryAndProcessResults(query, &elapsed_msec, &svpt_gone, false, NULL, NULL) > 0);
                --> PQsendQuery(pset.db, query);    // 通过libpq与数据库交互
                --> PQgetResult(pset.db);
        }
服务端

服务端相关流程:

main()
--> PostmasterMain(argc, argv);
    --> InitProcessGlobals();   // set MyProcPid, MyStartTime[stamp], random seeds
        --> pg_prng_strong_seed(&pg_global_prng_state)
        --> srandom(pg_prng_uint32(&pg_global_prng_state));
    --> InitializeGUCOptions();
    --> ProcessConfigFile(PGC_POSTMASTER);
        --> ProcessConfigFileInternal(context, true, elevel);
            --> ParseConfigFp(fp, abs_path, depth, elevel, head_p, tail_p);
    --> load_hba()  // 读pg_hba.conf
        --> parse_hba_line(tok_line, LOG)   // 解析pg_hba.conf中的每条记录,解析到HbaLine的链表结构中
    --> load_ident()
    --> ServerLoop();
        --> BackendStartup(port);
            --> InitPostmasterChild();
            --> BackendInitialize(port);
                --> pq_init();
                --> RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
	            --> enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
                --> ProcessStartupPacket(port, false, false);   // Read a client's startup packet
                    --> pq_startmsgread();
                    --> pq_getbytes((char *) &len, 1)
            --> InitProcess();
            --> BackendRun(port);
                --> PostgresMain(port->database_name, port->user_name);
                    --> BaseInit();
                    // 在接收用户SQL请求前进行认证
                    --> InitPostgres(dbname, InvalidOid, username, InvalidOid,!am_walsender, false, NULL);
                        --> PerformAuthentication(MyProcPort);
                            --> enable_timeout_after(STATEMENT_TIMEOUT, AuthenticationTimeout * 1000);
                            --> ClientAuthentication(port);
                                --> hba_getauthmethod(port);
                                    --> check_hba(port);
                                    // 根据不同的认证方法进行认证
                                --> switch (port->hba->auth_method)
	                                {		
                                        case uaTrust:
			                                    status = STATUS_OK;
			                                    break;
                                        case uaMD5:
		                                case uaSCRAM:
                                                // 口令认证
			                                    status = CheckPWChallengeAuth(port, &logdetail);
                                                         --> 
			                                    break;
                                        // ...
                                    }
                                --> if (status == STATUS_OK)
		                                sendAuthRequest(port, AUTH_REQ_OK, NULL, 0);
	                                else
		                                auth_failed(port, status, logdetail);
                            --> disable_timeout(STATEMENT_TIMEOUT, false);
                        --> initialize_acl();
                        // 认证通过后才能接受用户的SQL请求
                    --> for (;;)
                        {
                            // ...
                            exec_simple_query(query_string);
                        }

每次客户端发起连接时,postgres主进程都会fork一个子进程:

static int BackendStartup(Port *port)
{
    // ...
	pid = fork_process();
	if (pid == 0)				/* child */
	{
		free(bn);

		/* Detangle from postmaster */
		InitPostmasterChild();

		/* Close the postmaster's sockets */
		ClosePostmasterPorts(false);

		/* Perform additional initialization and collect startup packet */
		BackendInitialize(port);

		/*
		 * Create a per-backend PGPROC struct in shared memory. We must do
		 * this before we can use LWLocks. In the !EXEC_BACKEND case (here)
		 * this could be delayed a bit further, but EXEC_BACKEND needs to do
		 * stuff with LWLocks before PostgresMain(), so we do it here as well
		 * for symmetry.
		 */
		InitProcess();  

		/* And run the backend */
		BackendRun(port);  // 
	}
}

每个子进程在接收用户的SQL请求前,需要先进行认证。主要实现在ClientAuthentication函数中,通过认证才能继续进行下一步。

/*
 * Client authentication starts here.  If there is an error, this
 * function does not return and the backend process is terminated. */
void ClientAuthentication(Port *port)
{
	int			status = STATUS_ERROR;
	const char *logdetail = NULL;

	hba_getauthmethod(port);


	/*
	 * This is the first point where we have access to the hba record for the
	 * current connection, so perform any verifications based on the hba
	 * options field that should be done *before* the authentication here. */
	if (port->hba->clientcert != clientCertOff)
	{
		/* If we haven't loaded a root certificate store, fail */
		if (!secure_loaded_verify_locations())
			ereport(FATAL,
					(errcode(ERRCODE_CONFIG_FILE_ERROR),
					 errmsg("client certificates can only be checked if a root certificate store is available")));

		/* If we loaded a root certificate store, and if a certificate is
		 * present on the client, then it has been verified against our root
		 * certificate store, and the connection would have been aborted
		 * already if it didn't verify ok. */
		if (!port->peer_cert_valid)
			ereport(FATAL,(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("connection requires a valid client certificate")));
	}

    // 根据不同的认证方法,进行不同的处理
	switch (port->hba->auth_method)
	{
        // ...
		case uaMD5:
		case uaSCRAM:
			status = CheckPWChallengeAuth(port, &logdetail);
			break;

		case uaPassword:
			status = CheckPasswordAuth(port, &logdetail);
			break;

		case uaTrust:
			status = STATUS_OK;
			break;
	}

	if ((status == STATUS_OK && port->hba->clientcert == clientCertFull)|| port->hba->auth_method == uaCert)
	{
		/*
		 * Make sure we only check the certificate if we use the cert method
		 * or verify-full option.
		 */
#ifdef USE_SSL
		status = CheckCertAuth(port);
#else
		Assert(false);
#endif
	}

	if (ClientAuthentication_hook)
		(*ClientAuthentication_hook) (port, status);

	if (status == STATUS_OK)
		sendAuthRequest(port, AUTH_REQ_OK, NULL, 0);
	else
		auth_failed(port, status, logdetail);
}

对于口令认证,具体实现为CheckPWChallengeAuth函数。

/* MD5 and SCRAM authentication. */
static int CheckPWChallengeAuth(Port *port, const char **logdetail)
{
	int			auth_result;
	char	   *shadow_pass;
	PasswordType pwtype;

	/* First look up the user's password. */ // 查找pg_authid系统表中的用户密码
	shadow_pass = get_role_password(port->user_name, logdetail);

	if (!shadow_pass)
		pwtype = Password_encryption;
	else
		pwtype = get_password_type(shadow_pass);

	/* If 'md5' authentication is allowed, decide whether to perform 'md5' or
	 * 'scram-sha-256' authentication based on the type of password the user
	 * has.  If it's an MD5 hash, we must do MD5 authentication, and if it's a
	 * SCRAM secret, we must do SCRAM authentication.
	 *
	 * If MD5 authentication is not allowed, always use SCRAM.  If the user
	 * had an MD5 password, CheckSASLAuth() with the SCRAM mechanism will fail. */
	if (port->hba->auth_method == uaMD5 && pwtype == PASSWORD_TYPE_MD5)
		auth_result = CheckMD5Auth(port, shadow_pass, logdetail);
	else
		auth_result = CheckSASLAuth(&pg_be_scram_mech, port, shadow_pass,logdetail);

	if (shadow_pass)
		pfree(shadow_pass);

	/* If get_role_password() returned error, return error, even if the authentication succeeded. */
	if (!shadow_pass)
	{
		Assert(auth_result != STATUS_OK);
		return STATUS_ERROR;
	}

	if (auth_result == STATUS_OK)
		set_authn_id(port, port->user_name);

	return auth_result;
}

int CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass, const char **logdetail)
{
	StringInfoData sasl_mechs;
	int			mtype;
	StringInfoData buf;
	void	   *opaq = NULL;
	char	   *output = NULL;
	int			outputlen = 0;
	const char *input;
	int			inputlen;
	int			result;
	bool		initial;

	/*
	 * Send the SASL authentication request to user.  It includes the list of
	 * authentication mechanisms that are supported.
	 */
	initStringInfo(&sasl_mechs);

	mech->get_mechanisms(port, &sasl_mechs);
	/* Put another '\0' to mark that list is finished. */
	appendStringInfoChar(&sasl_mechs, '\0');

	sendAuthRequest(port, AUTH_REQ_SASL, sasl_mechs.data, sasl_mechs.len);
	pfree(sasl_mechs.data);

	/*
	 * Loop through SASL message exchange.  This exchange can consist of
	 * multiple messages sent in both directions.  First message is always
	 * from the client.  All messages from client to server are password
	 * packets (type 'p').
	 */
	initial = true;
	do
	{
		pq_startmsgread();
		mtype = pq_getbyte();
		if (mtype != 'p')
		{
			/* Only log error if client didn't disconnect. */
			if (mtype != EOF)
			{
				ereport(ERROR,(errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("expected SASL response, got message type %d",mtype)));
			}
			else
				return STATUS_EOF;
		}

		/* Get the actual SASL message */
		initStringInfo(&buf);
		if (pq_getmessage(&buf, PG_MAX_SASL_MESSAGE_LENGTH))
		{
			/* EOF - pq_getmessage already logged error */
			pfree(buf.data);
			return STATUS_ERROR;
		}

		elog(DEBUG4, "processing received SASL response of length %d", buf.len);

		/* The first SASLInitialResponse message is different from the others.
		 * It indicates which SASL mechanism the client selected, and contains
		 * an optional Initial Client Response payload.  The subsequent
		 * SASLResponse messages contain just the SASL payload. */
		if (initial)
		{
			const char *selected_mech;
			selected_mech = pq_getmsgrawstring(&buf);

			/*
			 * Initialize the status tracker for message exchanges.
			 *
			 * If the user doesn't exist, or doesn't have a valid password, or
			 * it's expired, we still go through the motions of SASL
			 * authentication, but tell the authentication method that the
			 * authentication is "doomed". That is, it's going to fail, no matter what.
			 *
			 * This is because we don't want to reveal to an attacker what
			 * usernames are valid, nor which users have a valid password. */
			opaq = mech->init(port, selected_mech, shadow_pass);

			inputlen = pq_getmsgint(&buf, 4);
			if (inputlen == -1)
				input = NULL;
			else
				input = pq_getmsgbytes(&buf, inputlen);

			initial = false;
		}
		else
		{
			inputlen = buf.len;
			input = pq_getmsgbytes(&buf, buf.len);
		}
		pq_getmsgend(&buf);

		/* Hand the incoming message to the mechanism implementation. */
		result = mech->exchange(opaq, input, inputlen,&output, &outputlen,logdetail);

		/* input buffer no longer used */
		pfree(buf.data);

		if (output)
		{
			/*PG_SASL_EXCHANGE_FAILURE with some output is forbidden by SASL.
			  Make sure here that the mechanism used got that right. */
			if (result == PG_SASL_EXCHANGE_FAILURE)
				elog(ERROR, "output message found after SASL exchange failure");

			/* Negotiation generated data to be sent to the client. */
			elog(DEBUG4, "sending SASL challenge of length %d", outputlen);

			if (result == PG_SASL_EXCHANGE_SUCCESS)
				sendAuthRequest(port, AUTH_REQ_SASL_FIN, output, outputlen);
			else
				sendAuthRequest(port, AUTH_REQ_SASL_CONT, output, outputlen);

			pfree(output);
		}
	} while (result == PG_SASL_EXCHANGE_CONTINUE);

	/* Oops, Something bad happened */
	if (result != PG_SASL_EXCHANGE_SUCCESS)
	{
		return STATUS_ERROR;
	}

	return STATUS_OK;
}

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

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

相关文章

商淘云:服装实体店引流会员营销方案

服装零售实体店面临着越来越大的挑战,尤其是在吸引和保持忠诚顾客方面。为了应对这一挑战,制定一套有效的引流会员营销方案显得尤为重要。商淘云将探讨如何通过创新的营销策略和增强的顾客体验,提升实体店的会员数量和销售业绩,从…

酒店会员寄存酒水管理方法,佳易王酒水寄存管理系统一卡管理多个商品操作教程

酒店会员寄存酒水管理方法,佳易王酒水寄存管理系统一卡管理多个商品操作教程 一、前言 以下软件操作教程以,佳易王酒店酒水寄存管理软件为例说明 软件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 1、会员项目设置操作教程 点击 导…

地图上绘制地铁线路

需求背景 不管是之前的pms 地铁还是location都会有需求涉及到地图上绘制地铁线路,来查看当前位置是否靠近地铁口,常规的交互可以看下高德地图,如图所示: 需求分析 不管是高德地图还是百度地图都提供了简易版的地铁线路图&#x…

从零开始! Jupyter Notebook的安装教程

🚀 从零开始! Jupyter Notebook的安装教程 摘要 📄 Jupyter Notebook 是一个广受欢迎的开源工具,特别适合数据科学和机器学习的开发者使用。本文将详细介绍从零开始安装 Jupyter Notebook 的步骤,包括各种操作系统的安装方法&am…

Fisnar Liquid Control 操作维修手LC Pump Manual Twinmixer Maintenance 中文

Fisnar Liquid Control 操作维修手LC Pump Manual Twinmixer Maintenance 中文

python读取excel中的图片超链接,批量下载到本地

1、代码 import xlrd import requestsread_path C:\\Users\\asus\\Desktop\\大法\\公务员\\国考\\行测\\1-推理判断\\URLs.xlsx bk xlrd.open_workbook(read_path) shxrange range(bk.nsheets) sh bk.sheet_by_name("Sheet2") nrows sh.nrows ncols sh.ncols …

Linux安装kvm虚拟机

kvm是基于内核的虚拟机,为什么要用kvm不用vmware、virtual box… 只有一个原因,它非常快!本机使用linux开发也是因为它快!linux在老电脑上都能流畅运行,更别说现代电脑,如果你觉得装Linux并没有比win快多少…

【网络安全学习】漏洞扫描:-01- 漏洞数据库searchsploit的使用

漏洞数据库是收集和存储各种软件漏洞信息的资源库。 漏洞数据库通常包含漏洞的名称、编号、描述、影响范围、危害等级、解决方案等信息,有些还提供漏洞的分析报告、演示视频、利用代码等内容。 1.常用的在线漏洞库: 国家信息安全漏洞共享平台 https:/…

pytorch基础【4】梯度计算、链式法则、梯度清零

文章目录 梯度计算计算图(Computational Graph)梯度求导(Gradient Computation)函数与概念 示例代码更多细节梯度求导的过程梯度求导的基本步骤示例代码注意事项总结 链式法则是什么?链式法则的数学定义链式法则在深度…

项目3:从0开始的RPC框架(扩展版)-3

七. 负载均衡 1. 需求分析 目前我们的RPC框架仅允许消费者读取第一个服务提供者的服务节点,但在实际应用中,同一个服务会有多个服务提供者上传节点信息。如果消费者只读取第一个,势必会增大单个节点的压力,并且也浪费了其它节点…

轻松解决App渠道合作痛点,Xinstall带参数安装让您事半功倍

在互联网流量红利逐渐衰退的今天,App推广和运营面临着前所未有的挑战。如何确保在多变的互联网环境下,迅速搭建起能时刻满足用户需求的运营体系,成为了众多企业急待解决的问题。Xinstall作为一款专注于解决App推广痛点的工具,通过…

企业如何选择合适的CRM工具?除Salesforce之外的10大主流选择

对比salesforce,其他10款优秀CRM:纷享销客CRM、Zoho CRM、腾讯企点、销售易、企业微信 (WeCom)、Odoo CR、OroCRM、金蝶、用友CRM、EspoCRM 虽然Salesforce以其全面的功能和强大的市场占有率在海外收获了许多客户,但Salesforce在国内市场的接…

就业市场挑战重重,求职者如何进入Salesforce生态系统?

目前,就业市场充满挑战,轻松进入Salesforce生态系统的日子已经一去不复返了。尽管入门级角色仍然存在,但市场上的申请者数量已超过其需求。成千上万的求职者争夺有限的职位,因此从人群中脱颖而出需要制定战略方法,求职…

集合系列(二十六) -利用LinkedHashMap实现一个LRU缓存

一、什么是 LRU LRU是 Least Recently Used 的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。 简单的说就是,对于一组数据,例如:int[] a {1,2,3,4,5,6},…

C++ 网络套接字编程 tcp udp

文章目录 网络通信的本质tcp 和 udp 协议网络字节序网络主机数据转化接口 udp 通信服务端逻辑客户端逻辑 TCP 通信服务端程序编写步骤客户端程序编写步骤 两种通信程序代码udp服务端程序编写udp 客户端程序编写tcp 服务端程序编写tcp 客户端程序编写 网络通信的本质 网络之间的…

香港电讯高可用网络助力企业变革金融计算

客户背景 客户是一家金融行业知名的量化私募对冲基金公司,专注于股票、期权、期货、债券等主要投资市场,在量化私募管理深耕多年,目前资管规模已达数百亿级,在国内多个城市均设有办公地点。 客户需求 由于客户业务倚重量化技术…

内网渗透-隧道搭建ssp隧道代理工具frp内网穿透技术

内网渗透-隧道搭建&ssp隧道代理工具&frp内网穿透 内网穿透:解决网络控制上线&网络通讯问题 隧道技术:解决不出网协议上线的问题(利用出网协议进行封装出网) 代理技术:解决网络通讯不通的问题&#xff0…

C++ —— unordered_set、unordered_map的介绍及使用

目录 unordered系列关联式容器 unordered_set的介绍 unordered_set的使用 unordered_set的定义方式 unordered_set接口的使用 unordered_multiset unordered_map的介绍 unordered_map的使用 unordered_map的定义方式 unordered_map接口的使用 unordered_multimap …

【两数之和】

两数之和 一、题目二、暴力解法三、哈希表四、map字典1.基本方法.set()添加键值对.get()通过键获取值.has()判断map是否有这个键 2.map和set的联系和区别共同点共同点MapSet 一、题目 二、暴力解法 三、哈希表 解题思路:将nums的元素依次以键值对的方式存储在map字典…

怎么看OV泛域名证书市场占有率提升

近年来,随着网络安全的逐渐普及,使用SSL证书的站点也逐渐增多。https证书也成为了当下最受欢迎的数字证书之一,主要是用于保护网站和应用程序的安全,并提升用户对网站的信任度,且只有企业或组织才可申请OV级别的SSL证书…