openGauss源码解析(193)
openGauss源码解析:安全管理源码解析(4)
9.2.3 认证机制
整个认证过程中身份认证完成后需要完成最后的认证识别。通过用户名和密码来验证数据库用户的身份,判断其是否为合法用户。openGauss使用基于RFC5802协议的口令认证方案,该方案是一套包含服务器和客户端双向认证的用户认证机制。
首先客户端知道用户名username和密码password,客户端发送用户名username给服务端,服务端检索相应的认证信息,例如:salt、StoredKey、ServerKey和迭代次数。然后服务端发送盐值salt和迭代次数给客户端。接下来客户端需要进行一些计算,给服务端发送ClientProof认证信息,服务端通过ClientProof对客户端进行认证,并发送ServerSignature给客户端。最后客户端通过ServerSignature对服务端进行认证。具体密钥计算代码如下所示:
SaltedPassword := Hi(password, salt, iteration_count) /*其中,Hi()本质上是PBKDF2*/
ClientKey := HMAC(SaltedPassword, "Client Key")
StoredKey := sha256(ClientKey)
ServerKey := HMAC(SaltedPassword, "Sever Key")
ClientSignature:=HMAC(StoredKey, token)
ServerSignature:= HMAC(ServerKey, token)
ClientProof:= ClientSignature XOR ClientKey
具体密钥衍生过程如图9-8所示。
图9-8 密钥衍生过程
服务器端存储的是StoredKey和ServerKey:
(1) StoredKey用来验证客户端用户身份。
服务端认证客户端通过计算ClientSignature与客户端发来的ClientProof进行异或运算,从而恢复得到ClientKey,然后将其进行HMAC(hash-based message authentication code,散列信息认证码)运算,将得到的值与StoredKey进行对比,如果相等,证明客户端验证通过。其中ClientSignature通过StoredKey和token(随机数)进行HMAC计算得到。
(2) ServerKey用来向客户端表明自己身份的。
客户端认证服务端,通过计算ServerSignature与服务端发来的值进行比较,如果相等,则完成对服务端的认证。其中ServerSignature通过ServerKey和token(随机数)进行HMAC计算得到。
(3) 在认证过程中,服务端可以计算出来ClientKey,验证完后直接丢弃不必存储。
防止服务端伪造认证信息ClientProof,从而仿冒客户端。
接下来详细描述在一个认证会话期间的客户端和服务端的信息交换过程。如图9-9所示。
图9-9 openGauss认证流程
认证流程为:
(1) 客户端发送username。
(2) 服务端返回盐值salt、iteration-count(迭代次数)、ServerSignature以及随机生成的字符串token给客户端。token是随机生成字符串。服务端通过计算得到的ServerSignature返回给客户端。
ServerSignature := HMAC(ServerKey, token)
(3) 客户端认证服务端并发送认证响应。响应信息包含客户端认证信息ClientProof。ClientProof证明客户端拥有ClientKey,但是不通过网络的方式发送。在收到信息后,计算ClientProof。
客户端利用salt和iteration-count,从password计算得到SaltedPassword,然后通过图9-9中的公式计算得到ClientKey、StoryKey和ServerKey。
客户端通过StoredKey和token进行哈希计算得到ClientSignature:
ClientSignature := HMAC(StoredKey,token)
通过将ClientKey和ClientSignature进行异或得到ClientProof:
ClientProof := ClientKey XOR ClientSignature
将计算得到的ClientProof和第(2)步接收的随机字符串发送给服务端进行认证。
(4) 服务端接收并校验客户端信息。
使用其保存的StoredKey和token通过HMAC算法进行计算,然后与客户端传来的ClientProof进行异或,恢复ClientKey;再对ClientKey进行哈希计算,得到的结果与服务端保存的StoredKey进行比较。如果相等则服务端对客户端的认证通过,否则认证失败。
ClientSignature := HMAC(StoredKey, token)
HMAC(ClientProof XOR ClientSignature ) = StoredKey
客户端认证的过程通过调用ClientAuthentication函数完成,该函数只有一个类型Port的参数,Port结构中存储着客户端相关信息,Port结构与客户端相关的部分字段参见“9.2.1 身份”章节介绍。完整的客户端认证过程见ClientAuthentication函数,代码如下所示:
void ClientAuthentication(Port* port)
{
int status = STATUS_ERROR;
char details[PGAUDIT_MAXLENGTH] = {0};
char token[TOKEN_LENGTH + 1] = {0};
errno_t rc = EOK;
GS_UINT32 retval = 0;
hba_getauthmethod(port);
……
switch (port->hba->auth_method) {
case uaReject:
……
case uaImplicitReject:
……
/* 使用MD5口令认证 */
case uaMD5:
sendAuthRequest(port, AUTH_REQ_MD5);
status = recv_and_check_password_packet(port);
break;
/* 使用sha256认证方法 */
case uaSHA256:
/* 禁止使用初始用户进行远程连接 */
if (isRemoteInitialUser(port)) {
ereport(FATAL,
(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("Forbid remote connection with initial user.")));
}
rc = memset_s(port->token, TOKEN_LENGTH * 2 + 1, 0, TOKEN_LENGTH * 2 + 1);
securec_check(rc, "\0", "\0");
HOLD_INTERRUPTS();
/* 生成随机数token */
retval = RAND_priv_bytes ((GS_UCHAR*)token, (GS_UINT32)TOKEN_LENGTH);
RESUME_INTERRUPTS();
CHECK_FOR_INTERRUPTS();
if (retval != 1) {
ereport(ERROR, (errmsg("Failed to Generate the random number,errcode:%u", retval)));
}
sha_bytes_to_hex8((uint8*)token, port->token);
port->token[TOKEN_LENGTH * 2] = '\0';
/* 发送认证请求到前端,认证码为AUTH_REQ_SHA256 */
sendAuthRequest(port, AUTH_REQ_SHA256);
/* 接收并校验客户端的信息 */
status = recv_and_check_password_packet(port);
break;
……
}
……
if (status == STATUS_OK)
sendAuthRequest(port, AUTH_REQ_OK);
else {
auth_failed(port, status);
}
/* 完成认证,关闭参数ImmediateInterruptOK */
t_thrd.int_cxt.ImmediateInterruptOK = false;
}
在这个ClientAuthentication函数中通过先后调用hba_getauthmethod函数、check_hba函数,检查客户端地址、所连接数据库、用户名在文件HBA中是否有能匹配的HBA记录(具体HBA及check_hba相关内容参见“9.2.1 身份”节)。如果能够找到匹配的HBA记录,则将Port结构中相关认证方法的字段设置为HBA记录中的参数,同时状态值为STATUS_OK。然后根据不同的认证方法,进行相应的认证过程。具体认证方法如表9-2所示,在认证过程中可能需要和客户端进行多次交互。最后返回如果为STAUS_OK,则表示认证成功,并将认证成功的信息发送回客户端,否则发送认证失败的信息。
表9-2 认证方法
认证方法 | 值 | 描述 |
uaReject | 0 | 无条件的拒绝连接 |
uaTrust | 3 | 无条件的允许连接,即允许匹配HBA记录的客户端连入数据库 |
uaMD5 | 5 | 要求客户端提供一个MD5加密口令进行认证 |
uaSHA256 | 6 | 要求客户端提供SHA256加密口令进行认证 |
uaGSS | 7 | 通过GSS-API(generic security service,通用安全服务;application programming interface,应用编程接口)认证用户 |
接下来介绍客户端认证服务端并发送认证响应。客户端根据不同的认证方法进行不同的处理过程,当前方法为AUTH_REQ_SHA256时,通过调用函数pg_password_sendauth完成对服务端的认证,代码如下所示:
static int pg_password_sendauth(PGconn* conn, const char* password, AuthRequest areq)
{
int ret;
/* 初始化变量 */
……
char h[HMAC_LENGTH + 1] = {0};
char h_string[HMAC_LENGTH * 2 + 1] = {0};
char hmac_result[HMAC_LENGTH + 1] = {0};
char client_key_bytes[HMAC_LENGTH + 1] = {0};
switch (areq) {
case AUTH_REQ_MD5:
/* pg_md5_encrypt()通过MD5Salt进行MD5加密 */
……
case AUTH_REQ_MD5_SHA256:
……
case AUTH_REQ_SHA256: {
char* crypt_pwd2 = NULL;
if (SHA256_PASSWORD == conn->password_stored_method || PLAIN_PASSWORD == conn->password_stored_method) {
/* 通过SHA256方式加密密码 */
if (!pg_sha256_encrypt(
password, conn->salt, strlen(conn->salt), (char*)buf, client_key_buf, conn->iteration_count))
return STATUS_ERROR;
rc = strncpy_s(server_key_string,
sizeof(server_key_string),
&buf[SHA256_LENGTH + SALT_STRING_LENGTH],
sizeof(server_key_string) - 1);
securec_check_c(rc, "\0", "\0");
rc = strncpy_s(stored_key_string,
sizeof(stored_key_string),
&buf[SHA256_LENGTH + SALT_STRING_LENGTH + HMAC_STRING_LENGTH],
sizeof(stored_key_string) - 1);
securec_check_c(rc, "\0", "\0");
server_key_string[sizeof(server_key_string) - 1] = '\0';
stored_key_string[sizeof(stored_key_string) - 1] = '\0';
sha_hex_to_bytes32(server_key_bytes, server_key_string);
sha_hex_to_bytes4(token, conn->token);
/* 通过server_key和token调用HMAC算法计算,得到client_server_signature_bytes,通过该变量转为字符串变量,用来验证与服务端传来的server_signature是否相等。 */
CRYPT_hmac_ret1 = CRYPT_hmac(NID_hmacWithSHA256,
(GS_UCHAR*)server_key_bytes,
HMAC_LENGTH,
(GS_UCHAR*)token,
TOKEN_LENGTH,
(GS_UCHAR*)client_server_signature_bytes,
(GS_UINT32*)&hmac_length);
if (CRYPT_hmac_ret1) {
return STATUS_ERROR;
}
sha_bytes_to_hex64((uint8*)client_server_signature_bytes, client_server_signature_string);
/* 调用函数strncmp判断计算的client_server_signature_string和服务端传来的server_signature值是否相等 */
if (PG_PROTOCOL_MINOR(conn->pversion) < PG_PROTOCOL_GAUSS_BASE &&
0 != strncmp(conn->server_signature, client_server_signature_string, HMAC_STRING_LENGTH)) {
pwd_to_send = fail_info; /* 不相等则认证失败 */
} else {
sha_hex_to_bytes32(stored_key_bytes, stored_key_string);
/* 通过stored_key和token计算得到hmac_result */
CRYPT_hmac_ret2 = CRYPT_hmac(NID_hmacWithSHA256,
(GS_UCHAR*)stored_key_bytes,
STORED_KEY_LENGTH,
(GS_UCHAR*)token,
TOKEN_LENGTH,
(GS_UCHAR*)hmac_result,
(GS_UINT32*)&hmac_length);
if (CRYPT_hmac_ret2) {
return STATUS_ERROR;
}
sha_hex_to_bytes32(client_key_bytes, client_key_buf);
/* hmac_result和client_key_bytes异或得到h,然后将其发送给服务端,用于验证客户端 */
if (XOR_between_password(hmac_result, client_key_bytes, h, HMAC_LENGTH)) {
return STATUS_ERROR;
}
sha_bytes_to_hex64((uint8*)h, h_string);
pwd_to_send = h_string; /* 设置要发送给服务端的值 */
}
}
……
break;
/* 清空变量 */
……
return ret;
}