鲲鹏社区首页
中文
注册
开发者
openGauss数据库源码解析系列文章——安全管理源码解析(二

openGauss数据库源码解析系列文章——安全管理源码解析(二

openGauss

发表于 2025/12/02

0

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

具体密钥衍生过程如图1所示。

图1 密钥衍生过程

服务器端存储的是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,从而仿冒客户端。

接下来详细描述在一个认证会话期间的客户端和服务端的信息交换过程。如图2所示。

图2 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,则表示认证成功,并将认证成功的信息发送回客户端,否则发送认证失败的信息。

表1 认证方法

认证方法描述
uaReject0无条件的拒绝连接
uaTrust3无条件的允许连接,即允许匹配HBA记录的客户端连入数据库
uaMD55要求客户端提供一个MD5加密口令进行认证
uaSHA2566要求客户端提供SHA256加密口令进行认证
uaGSS7通过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;
}

9.2.4 Kerberos安全认证

Kerberos是一种基于对称密钥技术的身份认证协议。开源组件Kerberos可以解决集群内节点或者进程之间的认证问题,即当开启kerberos之后,恶意用户无法仿冒集群内节点或进程来登录数据库系统,只有内部组件才可以持有用于认证的凭证。从而保证通过Kerberos认证,消减了仿冒风险,提升了数据库系统的安全性。Kerberos协议认证交互如图3所示。

图3 Kerberos认证标准交互流程

其中各角色和定义如表2所示(为下文描述方便均以缩写代替)。

表2 Kerberos协议角色

角色说明
KDC(key distribution center,密钥分发中心)Kerberos服务程序
Client需要访问服务的用户(principal),KDC和Service会对用户的身份进行认证
Service集成了Kerberos的服务,被访问的服务,需要对客户端进行认证
AS(authentication service,认证服务)AS服务器用于身份的校验, 内部会存储所有的账号信息
TGS(ticket granting service,票据授权服务)TGT(ticket-granting ticket)票据分发服务

openGauss可在数据库系统部署完毕之后开启Kerberos模式,即Kerberos服务部署在数据库系统机器上,部署过程中会开启Kerberos相关的服务,并派发凭证给集群内部所有的节点,初始化一系列Kerberos需要用到的环境变量,数据库内核中通过调用GSS-API来实现Kebreros标准协议的通信内容。以openGauss主备之间的认证为例,在Kerberos开启后openGauss内部进程之间认证流程如图4所示。

图4 数据库系统Kerberos认证流程

Kerberos提供用户(数据库管理员)透明的认证机制,数据库管理员无须感知Kerberos进程/部署情况。图9-11中分两部分描述Kerberos交互,左侧虚线框内的Kerberos协议实现部分由OM工具完成。OM工具在Kerberos初始化的时候将KDC服务拉起(krb5kdc进程),KDC服务内置了两个服务:AS和TGS服务。客户端(openGauss主备等数据库服务进程)在登录对端之前会先和KDC交互拿到TGT(ticket granting ticket,根凭证),这个步骤由OM拉起的定时任务调用Kerebros提供刷新票据工具来实现,默认24小时重新获取1次。该获取TGT的过程对应Kerberos标准协议中的AS-REQ、AS-REP、TGS-REQ和TGS-REP模块。

右侧侧虚线框内的数据库内侧认证,主要是图4右侧虚线框内的AP-REQ流程实现,简化流程如图5所示。

图5 数据库系统内核认证交互

数据库内核封装GSS-API数据结构,实现跟外部API交互认证,关键数据结构源代码文件为“src\include\libpq\auth.h”,相关代码如下:


typedef struct GssConn {
    int sock;
    gss_ctx_id_t gctx;        /*  GSS 上下文  */
    gss_name_t gtarg_nam;   /*  GSS 名称  */
    gss_buffer_desc ginbuf;   /*  GSS 输入token  */
    gss_buffer_desc goutbuf;  /*  GSS 输出token  */
} GssConn; 
/*  客户端、服务端接口,用于封装标准kerberos协议调用,其中客户端接口用于向服务端  */
/*  发起访问,同时响应服务端接口GssServerAuth发起的票据请求  */
int GssClientAuth(int socket, char* server_host);
int GssServerAuth(int socket, const char* krb_keyfile);


图6 数据库内核Kerberos认证时序图

认证交互逻辑时序如图6所示。认证流程如下。

(1) 服务端通过数据库配置文件决定使用Kerberos协议对客户端连接进行认证。

(2) 发起认证请求,客户端准备需要Kerberos认证的环境和票证,发’P’报文响应请求并发送票证。

(3) 服务端验证通过后会发送响应’R’报文,完成Kerberos认证。

以上内容为安全管理源码解析—安全认证中认证机制、Kerberos安全认证的相关内容,下篇图文,将详细介绍“角色管理”相关内容,敬请期待!