diff --git a/TestTasks/yuan-jianshuo/pic_0/SF_1.png b/TestTasks/yuan-jianshuo/pic_0/SF_1.png new file mode 100644 index 0000000000000000000000000000000000000000..8a5208d042fc10c925a2dab89e56af1d3b4243ef Binary files /dev/null and b/TestTasks/yuan-jianshuo/pic_0/SF_1.png differ diff --git a/TestTasks/yuan-jianshuo/pic_1/KouLing.png b/TestTasks/yuan-jianshuo/pic_1/KouLing.png new file mode 100644 index 0000000000000000000000000000000000000000..daf2fdc98a75e994a0401a35568c0a5b451e1495 Binary files /dev/null and b/TestTasks/yuan-jianshuo/pic_1/KouLing.png differ diff --git a/TestTasks/yuan-jianshuo/pic_1/SM3_1.png b/TestTasks/yuan-jianshuo/pic_1/SM3_1.png new file mode 100644 index 0000000000000000000000000000000000000000..2f25efdf35b64cc1b75ef10e2bddb4b0978fab00 Binary files /dev/null and b/TestTasks/yuan-jianshuo/pic_1/SM3_1.png differ diff --git a/TestTasks/yuan-jianshuo/pic_1/kouling3.jpg b/TestTasks/yuan-jianshuo/pic_1/kouling3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fee8f25a405498fdbc3f42843219c69d73d42300 Binary files /dev/null and b/TestTasks/yuan-jianshuo/pic_1/kouling3.jpg differ diff --git a/TestTasks/yuan-jianshuo/pic_1/kouling4.webp b/TestTasks/yuan-jianshuo/pic_1/kouling4.webp new file mode 100644 index 0000000000000000000000000000000000000000..6ed204cbd6ce6ad1d592b16b565be886dfa288d4 Binary files /dev/null and b/TestTasks/yuan-jianshuo/pic_1/kouling4.webp differ diff --git a/TestTasks/yuan-jianshuo/pic_2/SJ_1.png b/TestTasks/yuan-jianshuo/pic_2/SJ_1.png new file mode 100644 index 0000000000000000000000000000000000000000..b3625676e813947b5e8bdd11ab4f13a5d550edad Binary files /dev/null and b/TestTasks/yuan-jianshuo/pic_2/SJ_1.png differ diff --git a/TestTasks/yuan-jianshuo/pic_2/SJ_2.png b/TestTasks/yuan-jianshuo/pic_2/SJ_2.png new file mode 100644 index 0000000000000000000000000000000000000000..c3d77a7843c9c229c2cfe48747ec912473251112 Binary files /dev/null and b/TestTasks/yuan-jianshuo/pic_2/SJ_2.png differ diff --git a/TestTasks/yuan-jianshuo/pic_3/JS_1.png b/TestTasks/yuan-jianshuo/pic_3/JS_1.png new file mode 100644 index 0000000000000000000000000000000000000000..766cc82ef78df70b7f7fb5a812c930d8436acf1e Binary files /dev/null and b/TestTasks/yuan-jianshuo/pic_3/JS_1.png differ diff --git "a/TestTasks/yuan-jianshuo/\345\217\243\344\273\244\345\255\230\345\202\250.md" "b/TestTasks/yuan-jianshuo/\345\217\243\344\273\244\345\255\230\345\202\250.md" new file mode 100644 index 0000000000000000000000000000000000000000..6b702507d49dc15770fb433ced49b858bae807bc --- /dev/null +++ "b/TestTasks/yuan-jianshuo/\345\217\243\344\273\244\345\255\230\345\202\250.md" @@ -0,0 +1,520 @@ +### openGauss安全机制之口令存储 + +[TOC] + +#### 前言 + +本文《openGauss安全机制之口令存储》为第五届中国软件开源创新大赛——开源代码评注赛道参赛作品 + +作者:袁建硕 + +所属战队:共同学习 + +时间:2022-10-02 + +评注代码:[opengauss-mirror/openGauss-server: openGauss kernel (github.com)](https://github.com/opengauss-mirror/openGauss-server) + +*** + + + +#### 口令加密与存储概述 + +口令的加密与存储作为身份认证的重要一环,尤其是在面向庞大的价值数据面前,安全性与可靠性格外重要。本文主要分析openGauss对口令的操作与加密方式,并另外重点分析新增支持的SM3加密算法原理。 + +openGauss数据库在执行创建用户或修改用户口令操作时,会将口令通过**单向Hash**加密存储在pg_authid系统表中。口令的加密方式与加密参数配置在"password_encryption_type"中。 + +目前系统支持MD5、SHA256+MD5(同时存储两种加密值)、SHA256以及**SM3(新增)**四种方式,默认采用第三种SHA256加密方式。 + +> 这里解释为何要保留已经稍显落伍的MD5加密方式? +> +> 主要是考虑到了数据库体系结构的**向后兼容**,为了**兼容已经逐渐成熟的PostgreSQL**社区及其他工具,保留了安全性较低的MD5方式。 + + + +一张表概括各类加密类型、加密方式、认证方式、加密函数之间的关系: + +| password_encryption_type | 加密方式 | 认证方式(pg_hba.conf) | 加密函数接口 | +| ------------------------ | ---------- | --------------------- | -------------------------------------- | +| 0 | MD5 | MD5 | pg_md5_encrypt | +| 1 | SHA256+MD5 | SHA256或MD5 | calculate_encrypted_combinaed_password | +| 2(默认) | SHA256 | SHA256 | calculate_encrypted_sha256_password | +| 3(PASSWORD_TYPE_SM3) | SM3 | SM3 | gs_calculate_encrypted_sm3_password | + + + +#### 口令加密过程 + +口令加密的需求产生于**创建用户以及用户修改密码**。收到指令后,生成随机数作为**盐值Salt**。之后**检查**口令是否满足**复杂度的要求**(详情可见[补充](#补充)部分),满足口令复杂度的话,则去**执行口令加密函数**,**判断加密方式**进行加密,返回密文口令,**存储到pg_authid系统表**中,完成加密的全过程。 + +![kouling0](./pic_1/KouLing.png) + + + +#### 相关代码分析 + +##### 口令加密函数 → calculate_encrypted_password + +```cpp +/* gausskernel/optimizer/commands/user.cpp:6052-6092 */ +Datum calculate_encrypted_password(bool is_encrypted, const char* password, const char* rolname, + const char* salt_string) +{ + // 空密码 + if (password == NULL || password[0] == '\0') { + ereport(ERROR, (errcode(ERRCODE_INVALID_PASSWORD), errmsg("The password could not be NULL."))); + } + errno_t rc = EOK; + char encrypted_md5_password[MD5_PASSWD_LEN + 1] = {0}; + Datum datum_value; + + // 是否已加密 + if (!is_encrypted || isPWDENCRYPTED(password)) { + return CStringGetTextDatum(password); + } + + /* + * The guc parameter of u_sess.attr.attr_security.Password_encryption_type here may be 0, 1, 2. + * if Password_encryption_type is 0, the encrypted password is md5. + * if Password_encryption_type is 1, the encrypted password is sha256 + md5. + * if Password_encryption_type is 2, the encrypted password is sha256. + * 另外新增了了SM3的加密方式Password_encryption_type is PASSWORD_TYPE_SM3(其值为3) + */ + // MD5加密 + if (u_sess->attr.attr_security.Password_encryption_type == 0) { + if (!pg_md5_encrypt(password, rolname, strlen(rolname), encrypted_md5_password)) { + rc = memset_s(encrypted_md5_password, MD5_PASSWD_LEN + 1, 0, MD5_PASSWD_LEN + 1); + securec_check(rc, "\0", "\0"); + ereport(ERROR, (errcode(ERRCODE_INVALID_PASSWORD), errmsg("password encryption failed"))); + } + + datum_value = CStringGetTextDatum(encrypted_md5_password); + rc = memset_s(encrypted_md5_password, MD5_PASSWD_LEN + 1, 0, MD5_PASSWD_LEN + 1); + securec_check(rc, "\0", "\0"); + ereport(NOTICE, (errmsg("The encrypted password contains MD5 ciphertext, which is not secure."))); + } else if (u_sess->attr.attr_security.Password_encryption_type == 1) { // sha256+MD5 + datum_value = calculate_encrypted_combined_password(password, rolname, salt_string); + } else if (u_sess->attr.attr_security.Password_encryption_type == PASSWORD_TYPE_SM3) { //SM3加密 + datum_value = gs_calculate_encrypted_sm3_password(password, salt_string); + } else { // sha256 + datum_value = calculate_encrypted_sha256_password(password, rolname, salt_string); + } + + return datum_value; +} +``` + + + + + +##### MD5加密方式 + +对于MD5加密算法,本文便不详细介绍 + +> 可参考优质博客[ MD5 加密算法详解_红月修罗的博客-CSDN博客_md5是什么算法](https://blog.csdn.net/hawinlolo/article/details/94464237) + +MD5 算法将输入的信息进行分组,每组512 位(64个 字节),顺序处理完所有分组后输出128 位结果。 +在每一组消息的处理中,都要进行4 轮、每轮16 步、总计64 步的处理。其中每步计算中含一次左循环移位,每一步结束时将计算结果进行一次右循环移位。 + +执行函数:**pg_md5_encrypt( )** + +```cpp +/* common/backend/libpq/md5.cpp:308-349 */ +bool pg_md5_encrypt(const char* passwd, const char* salt, size_t salt_len, char* buf) +{ + ... +} +``` + + + +##### MD5加密和SHA256加密方式 + +**执行函数:calculate_encrypted_combined_password( )** + +```cpp +/* gausskernel/optimizer/commands/user.cpp:5904-5958 */ +Datum calculate_encrypted_combined_password(const char* password, const char* rolname, const char* salt_string) +{ + ... +} +``` + + + +##### SHA256加密方式 + +执行函数:**calculate_encrypted_sha256_password( )** + +```cpp +/* gausskernel/optimizer/commands/user.cpp:5960-6000 */ +Datum calculate_encrypted_sha256_password(const char* password, const char* rolname, const char* salt_string) +{ + ... +} +``` + + + +##### SM3加密方式 + +执行函数:**gs_calculate_encrypted_sm3_password( )** + +```cpp +/* gausskernel/optimizer/commands/user.cpp:6002-6042 */ +static Datum gs_calculate_encrypted_sm3_password(const char* password, const char* salt_string) +{ + ... +} +``` + + + +前面三种加密方式已经耳熟能详,互联网上也有了不少优质文章介绍。也正是因为这些加密算法大家耳熟能详,关注度高,这些**算法也逐渐被攻破**。为了保障信息安全,国家密码管理局于2010年制定并公布了一系列国产密码算法,简称国密算法。 + +而openGauss也基于这一考量,引入了国密SM3加密算法,但由于引入的时间较短,相关文章也是寥寥无几。本着前人栽树后人乘凉的互联网理念,在这里详细介绍一下国密SM3。 + +*** + +#### openGauss引入国密SM3加密算法 + +**SM3密码杂凑算法**是中国国家密码管理局2010年公布的中国商用密码杂凑算法标准。该算法于2012年发布为密码行业标准(GM/T 0004-2012),2016年发布为国家密码杂凑算法标准(GB/T 32905-2016)。 + +SM3适用于商用密码应用中的数字签名和验证,是在SHA-256基础上改进实现的一种算法,其安全性和SHA-256相当。SM3和MD5的迭代过程类似,也采用Merkle-Damgard结构。消息分组长度为512位,摘要值长度为256位。 + +整个算法的执行过程可以概括成四个步骤:**消息填充、消息扩展、迭代压缩、输出结果。** + + + +##### 1. 消息填充 + +SM3的消息扩展是以512位的数据分组作为输入。 + +对数据进行填充,填充方法同MD5的加密方式,填入“1”后,填入k个“0”,使得拓展长度满足 + +```c++ +( n + 1 + k ) mod 512 = 448 +``` + +那么为什么是448呢?因为后面要再填充一段64位长的空间存储数据的长度。 + +如图所示: + +SM31 + + + +##### 2. 消息扩展 + +将512位数据划分为16个消息字,利用第一个512位数据生成的16个消息字递推生成剩余的116个消息字 + +总共迭代产生132个消息字,将前68个消息字记为`Wj`,后64个消息字记为`Wj'`。 + +生成消息字伪代码如下 + +```cpp +def CF(): +for j = 16 to 67 + Wj = P1(W_[j-16] xor W_[j-9] xor (W_[j-3] <<< 15 ) ) xor (W_[j-13] <<< 7) xor W_[j-6] + +for j = 0 to 63 + Wj' = Wj xor W_[j+4] +``` + +如下图所展示的过程递归加密 + +![](./pic_1/kouling4.webp) + + + +##### 3. 迭代压缩 + +然后是分析具体的压缩函数: + +`前16个消息值`与`初值IV`作为输入进入压缩函数,得到的值`result1`作为下一次的输入和下一组`16个消息字` + +得到`result2`,依次递归得到最终的杂凑值。 + + + +压缩函数的细节如下伪代码: + +```cpp +ABCDEFGH = V_i +V_(i+1) = CF(V(i), B(i)) +FOR j = 0 TO 63 + SS1 = ((A <<< 12) + E + (T_j <<< j)) <<< 7 + SS2 = SS1 xor (A <<< 12) + TT1 = FFj(A,B,C) + D + SS2 + Wj' + TT2 = GGj(E,F,G) + H + SS1 + Wj + D = C + C = B <<< 9 + B = A + A = TT1 + H = G + G = F <<< 19 + F = E + E = P0(TT2) +``` + +压缩函数的流程图如图所示: + +![](./pic_1/kouling3.jpg) + + + +##### 4 .输出结果 + +最后由压缩函数的输出A、B、C、D、E、F、G、H八个变量拼接就可以得到最后的加密值啦! + + + +##### 5. 结合代码分析 + +在已经了解了国密SM3的加密算法原理之后,让我们结合openGauss的源码看看是如何实现算法细节的。 + +我们追踪到之前提到的sm3加密函数**gs_calculate_encrypted_sm3_password( )** + +```cpp +/* + * INPUT: 口令password,盐值salt_string + * OUTPUT: 口令密文datum_value +*/ + +static Datum gs_calculate_encrypted_sm3_password(const char* password, const char* salt_string) +{ + char encrypted_sm3_password[SM3_PASSWD_LEN + 1] = {0}; // length: 196 + char encrypted_sm3_password_complex[SM3_PASSWD_LEN + ITERATION_STRING_LEN + 1] = {0}; // length: 206 + // iteration_string_len 迭代字符串长度 + char iteration_string[ITERATION_STRING_LEN + 1] = {0}; // length: 12 + Datum datum_value; + errno_t rc = EOK; + + // 将各类参数传入核心加密函数GsSm3Encrypt中 + if (!GsSm3Encrypt(password, + salt_string, + strlen(salt_string), + encrypted_sm3_password, + NULL, + u_sess->attr.attr_security.auth_iteration_count)) { + rc = memset_s(encrypted_sm3_password, SM3_PASSWD_LEN + 1, 0, SM3_PASSWD_LEN + 1); + securec_check(rc, "\0", "\0"); + ereport(ERROR, (errcode(ERRCODE_INVALID_PASSWORD), errmsg("password encryption failed"))); + } + + ... + + return datum_value; +} +``` + + + +这里追踪一下常数,做出一些简单的计算已写入注释中: + +```CPP +SM3_PASSWD_LEN = (ENCRYPTED_STRING_LENGTH + SM3_LENGTH) = 195 + + SM3_LENGTH = 3 + ENCRYPTED_STRING_LENGTH = (HMAC_STRING_LENGTH + STORED_KEY_STRING_LENGTH + SALT_STRING_LENGTH) = 192 + HMAC_STRING_LENGTH = (HMAC_LENGTH * 2) = 64 + HMAC_LENGTH = 32 + STORED_KEY_STRING_LENGTH = (STORED_KEY_LENGTH * 2) = 64 + STORED_KEY_LENGTH = 32 + SALT_STRING_LENGTH = (SALT_LENGTH * 2) = 64 + SALT_LENGTH = 32 + + +ITERATION_STRING_LEN = 11 +``` + + + +继续追踪加密核心函数**GsSm3Encrypt()**: + +```cpp +/* + * INPUT: 口令password,盐值salt_s,盐值长度salt_len,密文缓冲区buf, + * 客户端密钥client_key_buf,迭代长度iteration_count + * OUTPUT: bool +*/ + +bool GsSm3Encrypt( + const char* password, const char* salt_s, size_t salt_len, char* buf, char* client_key_buf, int iteration_count) +{ + size_t password_len = 0; + char k[K_LENGTH + 1] = {0}; // K_LENGTH = 32 + char client_key[CLIENT_KEY_BYTES_LENGTH + 1] = {0}; // CLIENT_KEY_BYTES_LENGTH = 32 + char sever_key[HMAC_LENGTH + 1] = {0}; // HMAC_LENGTH = 32 + char stored_key[STORED_KEY_LENGTH + 1] = {0}; // STORED_KEY_LENGTH = 32 + char salt[SALT_LENGTH + 1] = {0}; // SALT_LENGTH = 32 + char serverKeyString[HMAC_LENGTH * ENCRY_LENGTH_DOUBLE + 1] = {0}; // HMAC_LENGTH = 32 + // ENCRY_LENGTH_DOUBLE = 2 + char stored_key_string[STORED_KEY_LENGTH * 2 + 1] = {0}; // STORED_KEY_LENGTH = 32 + int pkcs_ret; + int sever_ret; + int client_ret; + int hash_ret; + int hmac_length = HMAC_LENGTH; + int stored_key_length = STORED_KEY_LENGTH; + int total_encrypt_length; + char server_string[SEVER_STRING_LENGTH_SM3] = "Server Key"; + char client_string[CLIENT_STRING_LENGTH] = "Client Key"; + errno_t rc = 0; + + if (NULL == password || NULL == buf) { + return false; + } + + password_len = strlen(password); + /* Tranform string(64Bytes) to binary(32Bytes) */ + /* 转换一下格式 */ + sha_hex_to_bytes32(salt, (char*)salt_s); + /* calculate k */ + /* 这里我们之前介绍过,满足(n+1+k) mod 512 = 448 */ + // 生成消息认证码 + pkcs_ret = PKCS5_PBKDF2_HMAC((char*)password, + password_len, + (unsigned char*)salt, + SALT_LENGTH, + iteration_count, + (EVP_MD*)EVP_sha1(), + K_LENGTH, + (unsigned char*)k); + if (!pkcs_ret) { + rc = memset_s(k, K_LENGTH + 1, 0, K_LENGTH + 1); + SECUREC_CHECK(rc); + + return false; + } + + /* We have already get k ,then we calculate client key and server key, + * then calculate stored key by using client key */ + + /* calculate server rkey */ + /* 计算服务端密钥 */ + sever_ret = CRYPT_hmac(NID_hmacWithSHA256, + (GS_UCHAR*)k, + K_LENGTH, + (GS_UCHAR*)server_string, + SEVER_STRING_LENGTH_SM3 - 1, + (GS_UCHAR*)sever_key, + (GS_UINT32*)&hmac_length); + if (sever_ret) { + rc = memset_s(k, K_LENGTH + 1, 0, K_LENGTH + 1); + SECUREC_CHECK(rc); + + rc = memset_s(sever_key, HMAC_LENGTH + 1, 0, HMAC_LENGTH + 1); + SECUREC_CHECK(rc); + return false; + } + + /* calculate client key */ + /* 计算客户端密钥 */ + client_ret = CRYPT_hmac(NID_hmacWithSHA256, + (GS_UCHAR*)k, + K_LENGTH, + (GS_UCHAR*)client_string, + CLIENT_STRING_LENGTH - 1, + (GS_UCHAR*)client_key, + (GS_UINT32*)&hmac_length); + if (client_ret) { + rc = memset_s(k, K_LENGTH + 1, 0, K_LENGTH + 1); + SECUREC_CHECK(rc); + + rc = memset_s(client_key, CLIENT_KEY_BYTES_LENGTH + 1, 0, CLIENT_KEY_BYTES_LENGTH + 1); + SECUREC_CHECK(rc); + + rc = memset_s(sever_key, HMAC_LENGTH + 1, 0, HMAC_LENGTH + 1); + SECUREC_CHECK(rc); + + return false; + } + + /* 转换回64位 */ + if (NULL != client_key_buf) { + sha_bytes_to_hex64((uint8*)client_key, client_key_buf); + } + + hash_ret = EVP_Digest( + (GS_UCHAR*)client_key, HMAC_LENGTH, (GS_UCHAR*)stored_key, (GS_UINT32*)&stored_key_length, EVP_sm3(), NULL); + + if (!hash_ret) { + ... + // 安全检查 + return false; + } + + /* Mark the type in the stored string */ + ... + + /* We must clear the mem before we free it for the safe */ + /* 清理内存,以确保安全*/ + ... + + return true; +} + +``` + + + +可惜只是看到了加密的整体流程,没有追踪到算法的具体细节。 + +但根据注释 + +```cpp + /* calculate server rkey */ + sever_ret = CRYPT_hmac() + +GS_UINT32 CRYPT_hmac(GS_UINT32 ulAlgType, const GS_UCHAR* pucKey, GS_UINT32 upucKeyLen, const GS_UCHAR* pucData, + GS_UINT32 ulDataLen, GS_UCHAR* pucDigest, GS_UINT32* pulDigestLen) +{ + const EVP_MD* evp_md = get_evp_md_by_id(ulAlgType); + if (evp_md == NULL) { + return 1; + } +#ifndef WIN32 + if (!HMAC(evp_md, pucKey, (int)upucKeyLen, pucData, ulDataLen, pucDigest, pulDigestLen)) { + return 1; + } +#else + if (!HMAC(evp_md, pucKey, (int)upucKeyLen, pucData, ulDataLen, pucDigest, (unsigned int*)pulDigestLen)) { + return 1; + } +#endif + return 0; +} +``` + +大致可以猜测到函数HMAC就是加密的关键。而且自此,便无法继续追踪下去,通过查阅资料 + +[HMAC(Hash-based Message Authentication Code)实现原理 - yvivid - 博客园 (cnblogs.com)](https://www.cnblogs.com/yvivid/p/hmac_basic.html),可以确认,加密细节都在其中。 + +*** + +#### 总结 + +本文重点分析了openGauss安全机制中的口令存储部分。安全形势总是日新月异,只有不断进步、创新,才能稳稳的保护用户的数据安全。正如**openGauss2022年主要规划特性:性能,可靠,安全所描述的样子**,采用新的加密算法国密SM3是一个不错的选择,也希望openGauss可以一步一步做下去,为更多用户提供优质的开源数据库软件。 + + + +#### 补充 + +openGauss用户口令强度校验机制: + +- 包含大写字母(A-Z)的最少个数(password_min_uppercase) +- 包含小写字母(a-z)的最少个数(password_min_lowercase) +- 包含数字(0-9)的最少个数(password_min_digital) +- 包含特殊字符的最少个数(password_min_special) +- 密码的最小长度(password_min_length) +- 密码的最大长度(password_max_length) +- 至少包含上述四类字符中的三类。 +- 不能和用户名、用户名倒写相同,本要求为非大小写敏感。 +- 不能和当前密码、当前密码的倒写相同。 +- 不能是弱口令。 + +参数password_policy设置为1时表示采用密码复杂度校验,默认值为1。[点我返回](#口令加密过程) + + diff --git "a/TestTasks/yuan-jianshuo/\345\256\241\350\256\241\344\270\216\350\277\275\350\270\252.md" "b/TestTasks/yuan-jianshuo/\345\256\241\350\256\241\344\270\216\350\277\275\350\270\252.md" new file mode 100644 index 0000000000000000000000000000000000000000..da0b65a66c94b991da55dc0032c2411cea1d322b --- /dev/null +++ "b/TestTasks/yuan-jianshuo/\345\256\241\350\256\241\344\270\216\350\277\275\350\270\252.md" @@ -0,0 +1,391 @@ +## openGauss安全机制之审计与追踪 + +[TOC] + +### 前言 + +本文《openGauss安全机制之审计与追踪》为第五届中国软件开源创新大赛——开源代码评注赛道参赛作品 + +作者:袁建硕 + +所属战队:共同学习 + +时间:2022-10-05 + +评注代码:[opengauss-mirror/openGauss-server: openGauss kernel (github.com)](https://github.com/opengauss-mirror/openGauss-server) + +*** + +### 审计与追踪概述 + +随着信息泄露事件的频发,数据库安全产品逐渐进入大众视野。而**数据库审计(DBAudit)**作为数据库安全技术之一,以安全事件为中心,以全面审计和精确审计为基础,实时记录网络上的数据库活动,对数据库操作进行细粒度审计的合规性管理,对数据库遭受到的风险行为进行实时告警。为用户带来诸多价值: + ++ 满足合规要求,加强监管能力 ++ 数据库为核心,操作全方位掌控 ++ 行为准确定位,便于追查定责 ++ 攻击精准识别,保证数据库系统安全 + +数据库的审计一定程度上起到威慑作用,将审计作为证据,依靠法律武器积极维权,也是保护数据安全的重要措施之一。 + + + +### 审计日志设计 + +首先,我们先了解审计日志的结构设计,审计日志通常是存储在数据库的表中或者系统文件中,openGauss在设计上选择了后者,更好的利用了操作系统的文件权限管理。 + +审计日志文件受操作系统权限的保护,默认只有初始化用户可以读写,保证了审计结果的安全性,避免被攻击篡改日志。 + +由源码以及命名规则不难得知,每条审计记录对应一个**AuditData**结构体 + +```cpp +typedef struct AuditData { + AuditMsgHdr header; /* 记录文件头,存储记录的表示、大小等 */ + AuditType type; /* 审计类型 */ + AuditResult result; /* 执行结果 */ + char varstr[1]; /* 二进制格式存储的具体审计信息 */ +} AuditData; +``` + +然后我们展开分析其中的数据结构 + +**AuditMsgHdr、AuditType、AuditResult** + +#### AuditMsgHdr + +```cpp +typedef struct AuditMsgHdr { + char signature[2]; /* 审计记录标识,目前固定为AUDIT前两个字符'A'和'U' */ + uint16 version; /* 版本信息,值为0 */ + uint16 fields; /* 审计记录字段数,值为13 */ + uint16 flags; /* 记录有效性标识,如果被删除则标记位DEAD */ + pg_time_t time; /* 审计记录创建时间 */ + uint32 size; /* 审计信息占字节长度 */ +} AuditMsgHdr; +``` + + + +#### AuditType + +都是一些常数,体现在日志的type字段 + +```cpp +typedef enum { + AUDIT_UNKNOWN_TYPE = 0, + AUDIT_LOGIN_SUCCESS, + AUDIT_LOGIN_FAILED, + AUDIT_USER_LOGOUT, + AUDIT_SYSTEM_START, + AUDIT_SYSTEM_STOP, + ... +} AuditType; +``` + + + +#### AuditResult + +体现在result字段中 + +```cpp +typedef enum { AUDIT_UNKNOWN = 0, AUDIT_OK, AUDIT_FAILED } AuditResult; +``` + + + +#### 示例中的其他问题 + +再回过头来看**审计日志单条信息示例** + +```sql +time | 时间 +type | 类型 +result | 结果 +userid | 用户id +username | 用户名 +database | 数据库名 +client_connifo | 客户端连接信息 +object_name | 目标名 +detail_info | 目标详细信息 +node_name | 结点名 +thread_id | 线程号 +local_port | 本地端口号 +remote_port | 远程端口号 +``` + + + +那么除了前面的三个字段,**其他字段**在哪呢? + +我们在pgaudit.cpp文件找到这一段函数 + +```cpp +static void deserialization_to_tuple(Datum (&values)[PGAUDIT_QUERY_COLS], + AuditData *adata, + const AuditMsgHdr &header) +{ + /* append timestamp info to data tuple */ + int i = 0; + values[i++] = TimestampTzGetDatum(time_t_to_timestamptz(adata->header.time)); + values[i++] = CStringGetTextDatum(AuditTypeDesc(adata->type)); + values[i++] = CStringGetTextDatum(AuditResultDesc(adata->result)); + + /* + * new format of the audit file under correct record + * the older audit file do not have userid info, so let it to be null + */ + int index_field = 0; + const char* field = NULL; + bool new_version = (header.fields == PGAUDIT_QUERY_COLS); + field = new_version ? pgaudit_string_field(adata, index_field++) : NULL; + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* user id */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* user name */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* dbname */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* client info */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* object name */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* detail info */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* node name */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* thread id */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* local port */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* remote port */ + + Assert(i == PGAUDIT_QUERY_COLS); +} +``` + +可以看到,后面的所有字段都是在这里被加入的 + + + +#### 日志组的设计 + +我们已经了解单条审计信息的字段格式 + +那么数据库操作中大量的日志又是**如何进行索引**的呢? + +Answer:这里采用了指针的结构。 + +首先有**索引表** + +> ​ [ 索引表 ] +> +> 索引表头|| 索引元素1 | 索引元素2 | 索引元素3 | ...... + + + +然后由**每个索引元素**链接到**对应的审计文件表**中 + +> ​ [ 审计文件表 ] +> +> 审计记录1 | 审计记录2 | 审计记录3 | ...... + + + +最后每个审计文件表中的结构就和之前示例中的相一致啦 + +> ​ [ 审计记录 ] +> +> 审计记录头 | 审计类行 | 审计结果 | 具体审计数据 + +如图所示: + +![Shenji1](pic_2/SJ_1.png) + +然后让我们联系代码去看一看 + + + +#### 联系代码 + +首先看**索引表的结构体** + +```cpp +typedef struct AuditIndexTable { + uint32 maxnum; /* 表中可存储的审计文件的最大数量 */ + uint32 begidx; /* 首个审计文件的位置 */ + uint32 curidx; /* 当前操作的审计文件的位置 */ + uint32 count; /* 当前表中存储的审计文件的数量 */ + pg_time_t last_audit_time; /* 最后一次写入审计记录的时间 */ + AuditIndexItem data[1]; /* 审计文件指针 */ +} AuditIndexTable; +``` + + + + + +然后我们继续追踪**审计文件的结构体**AuditIndexItem + +```cpp +typedef struct AuditIndexItem { + pg_time_t ctime; /* 审计文件创建时间 */ + uint32 filenum; /* 审计文件的名 */ + uint32 filesize; /* 审计文件的大小 */ +} AuditIndexItem; +``` + + + +这里以审计记录的写入函数audit_report为例 + +```cpp +/* + * Brief : 向系统审计员报告审计信息 + * Description : 由后端调用,通常以一下过程进行 + * 1. 验证类型和进程,决定是否去报告审计 + * 2. 从连接中得到所有审计信息 + * 3. 添加审计信息到字符缓冲区中 + * 4. 最后,写入审计文件或者发送给审计员进程去处理 + */ + +void audit_report(AuditType type, AuditResult result, const char *object_name, const char *detail_info, + AuditClassType ctype) +{ + /* 检查进程状态 */ + if (!audit_status_check_ok() || (detail_info == NULL)) { + return; + } + + /* 检查审计类型 */ + if (!audit_type_validcheck(type)) { + return; + } + + /* 从端口中得到信息 */ + StringInfoData buf; + AuditData adata; + AuditEventInfo event_info; + if (!audit_get_clientinfo(type, object_name, event_info)) { + return; + } + char *userid = event_info.userid; // 用户id + const char* username = event_info.username; // 用户名 + const char* dbname = event_info.dbname; // 数据库名 + char* client_info = event_info.client_info; // 客户端信息 + char* threadid = event_info.threadid; // 进程号 + char* localport = event_info.localport; // 本地端口 + char* remoteport = event_info.remoteport; // 远程端口 + + /* append xid info when audit_xid_info = 1 */ + char *detail_info_xid = NULL; + bool audit_xid_info = (u_sess->attr.attr_security.audit_xid_info == 1); + if (audit_xid_info) { + uint32 len = uint64_max_len + strlen("xid=, ") + strlen(detail_info) + 1; + detail_info_xid = (char *)palloc0(len); + audit_append_xid_info(detail_info, detail_info_xid, len); + } + + /* 数据头赋值 */ + adata.header.signature[0] = 'A'; + adata.header.signature[1] = 'U'; + adata.header.version = 0; + adata.header.fields = PGAUDIT_QUERY_COLS; + adata.header.flags = AUDIT_TUPLE_NORMAL; + adata.header.time = current_timestamp(); + adata.header.size = 0; + adata.type = type; + adata.result = result; + initStringInfo(&buf); + appendBinaryStringInfo(&buf, (char*)&adata, AUDIT_HEADER_SIZE); + + /* 审计数据赋值 */ + appendStringField(&buf, userid); + appendStringField(&buf, username); + appendStringField(&buf, dbname); + appendStringField(&buf, (client_info[0] != '\0') ? client_info : NULL); + appendStringField(&buf, object_name); + appendStringField(&buf, (!audit_xid_info) ? detail_info : detail_info_xid); + appendStringField(&buf, g_instance.attr.attr_common.PGXCNodeName); + appendStringField(&buf, (threadid[0] != '\0') ? threadid : NULL); + appendStringField(&buf, (localport[0] != '\0') ? localport : NULL); + appendStringField(&buf, (remoteport[0] != '\0') ? remoteport : NULL); + + /* + * Use the chunking protocol if we know the syslogger should be + * catching stderr output, and we are not ourselves the syslogger. + * Otherwise, just do a vanilla write to stderr. + */ + if (WRITE_TO_AUDITPIPE) { + write_pipe_chunks(buf.data, buf.len, ctype); + } else if (WRITE_TO_STDAUDITFILE(ctype)) { + pgaudit_write_file(buf.data, buf.len); + } else if (WRITE_TO_UNIAUDITFILE(ctype)) { + pgaudit_write_policy_audit_file(buf.data, buf.len); + } else if (detail_info != NULL) { + ereport(LOG, (errmsg("discard audit data: %s", (!audit_xid_info) ? detail_info : detail_info_xid))); + } + + if (detail_info_xid != NULL) { + pfree(detail_info_xid); + } + pfree(buf.data); +} +``` + + + +### 审计执行的原理 + +openGauss提供对用户发起的SQL行为审计和追踪能力,支持对DDL、DML语句和关键行为的审计。 + +> 简单介绍一下DDL语句与DML语句: +> +> DML(Data Manipulation Language)数据操纵语言: +> +> 适用范围:对数据库中的数据进行一些简单操作,如insert,delete,update,select等. +> +> +> +> DDL(Data Definition Language)数据定义语言: +> +> 适用范围:对数据库中的某些对象(例如,database,table)进行管理,如Create,Alter和Drop. +> + +工作线程初始化时便加载了审计模块,审计的执行原理是将审计函数赋给SQL生命周期不同阶段的HOOK函数。 + +同样在阅读代码之后,定位到了加载审计模块的关键函数**pgaudit_agent_init( )** + +```cpp +/* + * Brief : perfstat_agent_init() + * Description : Module load callback. + * Notes : Called from postmaster. + */ +void pgaudit_agent_init(void) +{ + if (!IsPostmasterEnvironment || !u_sess->attr.attr_security.Audit_enabled || + u_sess->exec_cxt.g_pgaudit_agent_attached) { + return; + } + prev_ExecutorEnd = ExecutorEnd_hook; + ExecutorEnd_hook = pgaudit_ExecutorEnd; + set_pgaudit_prehook(ProcessUtility_hook); + ProcessUtility_hook = (ProcessUtility_hook_type)pgaudit_ProcessUtility; + u_sess->exec_cxt.g_pgaudit_agent_attached = true; +} +``` + +SQL语句在指向到ProcessUtility_hook和ExecutorEnd_hook 函数指针时,会分别进入预置好的审计流程中。 + +这两个函数指针的位置在SQL进入执行器执行之前,具体关系如图: + +![Shenji2](pic_2/SJ_2.png) + +在计划期到执行器的途中,生成了计划树,此时审计模块则调用pgaudit_ExecutorEnd和pgaudit_ProcessUtility函数分别对DML语句和DDL语句进行分析。 + +*** + +### 总结 + +本文概述了openGauss安全机制中的审计与追踪,主要包括审计日志的设计以及审计执行的原理。 + +通过审计的的确确有效地维护了数据库的安全。但对于审计文件而已,过于依赖了操作系统对文件的保护,是openGuass仍然可以改进的地方。 diff --git "a/TestTasks/yuan-jianshuo/\350\247\222\350\211\262\345\210\233\345\273\272\343\200\201\347\256\241\347\220\206\344\270\216\350\256\244\350\257\201.md" "b/TestTasks/yuan-jianshuo/\350\247\222\350\211\262\345\210\233\345\273\272\343\200\201\347\256\241\347\220\206\344\270\216\350\256\244\350\257\201.md" new file mode 100644 index 0000000000000000000000000000000000000000..e2efb6cdcd0f8115c7ee08c6ff86ac9f17fc400f --- /dev/null +++ "b/TestTasks/yuan-jianshuo/\350\247\222\350\211\262\345\210\233\345\273\272\343\200\201\347\256\241\347\220\206\344\270\216\350\256\244\350\257\201.md" @@ -0,0 +1,1082 @@ +## openGauss安全机制之角色创建、管理与认证 + +[TOC] + +### 前言 + +本文《openGauss安全机制之角色创建、管理与认证》为第五届中国软件开源创新大赛——开源代码评注赛道参赛作品 + +作者:袁建硕 + +所属战队:共同学习 + +时间:2022-09-28 + +评注代码:[opengauss-mirror/openGauss-server: openGauss kernel (github.com)](https://github.com/opengauss-mirror/openGauss-server) + +*** + +### 角色创建与角色管理概述 + +数据库中,用户要想访问具体的数据库对象,需要各类复杂的权限,而如果采用逐个权限授予用户的方式,则会非常麻烦并且容易出错,不利于权限的管理。因此openGauss采用基于角色进行权限的管理,这种方式则能够实现权限的高效管理。 + +>什么是角色? +> +>角色是一组权限的集合,可以被赋予指定的权限并分配给用户,拥有角色的用户也就拥有了角色对应的权限。(注意:角色没有登陆权限,无法通过角色登录数据库) + +在数据库中,各种各样的角色拥有不同的权限,不同的操作,当对角色进行权限的授予和撤销时将会影响拥有该角色的所有用户。因此,对角色的区分管理及安全管控十分重要。 + +*** + +### 角色创建 + +角色是拥有数据库对象和权限的实体,在不同的环境中角色可以是一个用户、一个组或者两者均有。 + +在openGuass上创建一个角色,可以使用SQL命令CREATE ROLE + +```sql +CREATE ROLE role_name [ [ WITH ] option [ ... ] ] [ ENCRYPTED | UNENCRYPTED ] { PASSWORD | IDENTIFIED BY } { 'password' | DISABLE}; +``` + + + +创建角色调用函数CreateRole实现 + +```cpp +/* gausskernel/optimizer/commands/user.cpp: 548-1555 */ +void CreateRole(CreateRoleStmt* stmt) +{ + ... +} +``` + + + +其传入参数的结构体为: + +```cpp +typedef struct CreateRoleStmt { + NodeTag type; + RoleStmtType stmt_type; /* ROLE/USER/GROUP 创建角色的类型 角色/用户/组用户 */ + char* role; /* role name 角色名称 */ + List* options; /* List of DefElem nodes 角色属性列表,为一个链表结构 */ +} CreateRoleStmt; +``` + + + +然后查看函数声明中出现频率最高的结构体DefElem + +```cpp +typedef struct DefElem { + NodeTag type; + char *defnamespace; /* 节点对应的命名空间 */ + char *defname; /* 节点对应的角色属性名 */ + Node *arg; /* 表示值或类型名 */ + DefElemAction defaction; /* SET/ADD/DROP等其他未指定的行为 */ + int begin_location; /* token begin location, or -1 if unknown */ + int end_location; /* token end location, or -1 if unknown */ + int location; +} DefElem; +``` + + + +#### 创建流程 + +流程如图所示: + + +![](\pic_3\JS_1.png) + +##### 1.判断创建的角色类型 + +仍然是函数**CreateRole**函数中 + +```cpp +switch (stmt->stmt_type) { + case ROLESTMT_ROLE: + break; + case ROLESTMT_USER: + canlogin = true; + break; + case ROLESTMT_GROUP: + break; + default: + break; + } +``` + +当所创建的角色类型为用户时,将canlogin设置为true,这是因为用户默认具有登录权限。 + + + +##### 2. 循环获取角色属性options + +CreateRole: 657-894 + +从源码中可以看到 + +大量使用了strcmp判断各类属性的是否存在以及其value + +包括password口令、encryptedPassword加密口令、sysid系统号等字段 + +```cpp +/* 从Node tree中获取option */ +foreach (option, stmt->options) +{ + ... +} +``` + + + +##### 3.将获取的属性转换成对应的数据类型 + +CreateRole: 896-1166 + +采用了大量的if判断结构将对应的参数信息转换为需要的角色属性值类型 + +```cpp +... + if (dinherit != NULL) + inherit = intVal(dinherit->arg) != 0; + if (dcreaterole != NULL) + createrole = intVal(dcreaterole->arg) != 0; + if (dcreatedb != NULL) + createdb = intVal(dcreatedb->arg) != 0; + if (duseft != NULL) + useft = intVal(duseft->arg) != 0; + if (dcanlogin != NULL) + canlogin = intVal(dcanlogin->arg) != 0; + if (disreplication != NULL) + isreplication = intVal(disreplication->arg) != 0; +... +``` + + + +##### 4. 将要创建的角色属性值构建pg_authid元组 + +CreateRole: 1168-1178 + +```cpp +/* + * 检查pg_authid是否已经存在 + */ + Relation pg_authid_rel = heap_open(AuthIdRelationId, RowExclusiveLock); + TupleDesc pg_authid_dsc = RelationGetDescr(pg_authid_rel); + + if (OidIsValid(get_role_oid(stmt->role, true))) { + str_reset(password); + ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), errmsg("role \"%s\" already exists", stmt->role))); + } +``` + +之后就是检验时间戳是否超时以及对口令的再次检查 + +CreateRole: 1233-1371 + +创建一个插入的元组 + +```cpp +/* + * Build a tuple to insert + */ + errno_t errorno = memset_s(new_record, sizeof(new_record), 0, sizeof(new_record)); + securec_check(errorno, "\0", "\0"); + errorno = memset_s(new_record_nulls, sizeof(new_record_nulls), false, sizeof(new_record_nulls)); + securec_check(errorno, "\0", "\0"); + + new_record[Anum_pg_authid_rolname - 1] = DirectFunctionCall1(namein, CStringGetDatum(stmt->role)); + + new_record[Anum_pg_authid_rolsuper - 1] = BoolGetDatum(issuper); + new_record[Anum_pg_authid_rolinherit - 1] = BoolGetDatum(inherit); + new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(createrole); + new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(createdb); + ... +``` + + + +##### 5. 将tuple写入系统表并更新 + +CreateRole: 1372-1413 + +```cpp + HeapTuple tuple = heap_form_tuple(pg_authid_dsc, new_record, new_record_nulls); + + if (u_sess->proc_cxt.IsBinaryUpgrade && OidIsValid(u_sess->upg_cxt.binary_upgrade_next_pg_authid_oid)) { + HeapTupleSetOid(tuple, u_sess->upg_cxt.binary_upgrade_next_pg_authid_oid); + u_sess->upg_cxt.binary_upgrade_next_pg_authid_oid = InvalidOid; + } + + /* + * Insert new record in the pg_authid table + */ + roleid = simple_heap_insert(pg_authid_rel, tuple); + + /* add dependency of roleid on rpoid, no need add dependency on default_pool */ + if (IsUnderPostmaster) { + if (OidIsValid(rpoid) && (rpoid != DEFAULT_POOL_OID)) + recordDependencyOnRespool(AuthIdRelationId, roleid, rpoid); + + u_sess->wlm_cxt->wlmcatalog_update_user = true; + } + ... +``` + + + +##### 6. 将新创建的角色加入指定存在的父角色中 + +CreateRole: 1414-1430 + +```cpp +/* + * Add the new role to the specified existing roles. + */ + foreach (item, addroleto) { + char* oldrolename = strVal(lfirst(item)); + Oid oldroleid = get_role_oid(oldrolename, false); + + AddRoleMems( + oldrolename, oldroleid, list_make1(makeString(stmt->role)), list_make1_oid(roleid), GetUserId(), false); + } + + /* + * Add the specified members to this new role. adminmembers get the admin + * option, rolemembers don't. + */ + AddRoleMems(stmt->role, roleid, adminmembers, roleNamesToIds(adminmembers), GetUserId(), true); + AddRoleMems(stmt->role, roleid, rolemembers, roleNamesToIds(rolemembers), GetUserId(), false); +``` + + + +到此为止,完成了整个角色创建的过程 + + + +### 角色管理 + +角色管理主要包括以下内容: + ++ 修改角色属性 ++ 删除角色 ++ 授予和回收角色 + + + +#### 修改角色属性 + +SQL语句 ALTER ROLE修改数据库角色 + +通过调用AlterRole函数来实现角色属性的修改 + +查看关键结构体AlterRoleStmt + +```cpp +typedef struct AlterRoleStmt { + NodeTag type; + char* role; /* 角色名称 */ + List* options; /* 需要修改的属性列表 */ + int action; /* +1 = 增加成员关系, -1 = 删除成员关系 */ + RoleLockType lockstatus; /* 角色锁定状态 */ +} AlterRoleStmt; +``` + +大致流程为: + +AlterRole → 循环提取要修改的属性options → 转换为对应的数据类型 → 判断角色是否存在 → 判断角色是否有修改的权限 → 更新后的属性值同步到新元组中 → 新元组替代旧元组 → 判断成员关系action → 关闭系统表 → 结束 + + + +其结构与**角色创建**时大同小异 + +这里主要分析后两次判断: + +- 判断角色是否有修改的权限 +- 判断成员关系action + + + +**判断角色是否有修改的权限:** + +AlterRole:2258-2357 + +不同操作要求的权限不同,权限不足会报错提示 + +```cpp +/* Database Security: Support separation of privilege.*/ + if (roleid == BOOTSTRAP_SUPERUSERID) { + if (!(issuper < 0 && inherit < 0 && createrole < 0 && createdb < 0 && canlogin < 0 && isreplication < 0 && + isauditadmin < 0 && issystemadmin < 0 && ismonitoradmin < 0 && isoperatoradmin < 0 && + ispolicyadmin < 0 && isvcadmin < 0 && useft < 0 && ispersistence < 0 && dconnlimit == NULL && + rolemembers == NULL && validBegin == NULL && validUntil == NULL && drespool == NULL && + dparent == NULL && dnode_group == NULL && dspacelimit == NULL && dtmpspacelimit == NULL && + dspillspacelimit == NULL)) { + str_reset(password); + str_reset(replPasswd); + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("Permission denied to change privilege of the initial account."))); + } + } + ... +``` + + + +**判断成员关系action:** + +AlterRole:2999-3002 + +```cpp + if (stmt->action == +1) /* add members to role */ + AddRoleMems(stmt->role, roleid, rolemembers, roleNamesToIds(rolemembers), GetUserId(), false); + else if (stmt->action == -1) /* drop members from role */ + DelRoleMems(stmt->role, roleid, rolemembers, roleNamesToIds(rolemembers), false); +``` + + + + + +#### 删除角色 + +SQL命令:DROP ROLE + +通过函数DropRole实现,其对应结构体DropRoleStmt + +```cpp +typedef struct DropRoleStmt { + NodeTag type; + List* roles; /* 要删除的角色列表 */ + bool missing_ok; /* 判断角色是否存在 */ + bool is_user; /* 删除的时角色还是用户 */ + bool inherit_from_parent; /* 是否继承父角色 */ + DropBehavior behavior; /* 是否级联删除依赖对象 */ +} DropRoleStmt; +``` + +删除的大致流程为: + +判断是否有删除角色的权限 → 检查要删除的角色是否存在 → 再次检查是否有权删除此角色 → 此角色是否有依赖对象 → 删除pg_authid中对应的元组,删除pg_auth_member中相关元组 → 关闭系统表 → 结束 + + + +**注意两次删除权限的判断**,第一次是当前角色是否可以执行删除的操作,第二次是当前角色是否有权限删除目标角色 + +从逻辑上讲,也可以先循环处理删除的角色,判断要删除角色是否存在,再依次进行两次判断,但这样操作的话明显增加了复杂度,属于是没有必要 + + + +user.cpp : 3098-3383 + +函数have_createrole_privilege()检查是否有权删除 + +下面这段代码校验执行者和被删除角色的权限 + +```cpp +if ((((Form_pg_authid)GETSTRUCT(tuple))->rolsuper || ((Form_pg_authid)GETSTRUCT(tuple))->rolsystemadmin) && + !isRelSuperuser()) + ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("Permission denied."))); + + Datum datum = heap_getattr(tuple, Anum_pg_authid_roloperatoradmin, pg_authid_dsc, &isNull); + if (!isNull) { + is_opradmin = DatumGetBool(datum); + } else if (roleid == BOOTSTRAP_SUPERUSERID) { + is_opradmin = true; + } + if (is_opradmin && !initialuser()) { + ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("Permission denied."))); + } + + /* Forbid createrole holders to drop auditadmin when PrivilegesSeparate enabled. */ + if ((((Form_pg_authid)GETSTRUCT(tuple))->rolauditadmin) && + g_instance.attr.attr_security.enablePrivilegesSeparate && !isRelSuperuser()) + ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("Permission denied."))); + +``` + + + +针对级联的情况,删除该角色拥有的对象 + +```cpp + /* + * Drop the objects owned by the role if its behavior mod is CASCADE + */ + if (stmt->behavior == DROP_CASCADE) { + char* user = NULL; + + CancelQuery(role); + user = (char*)palloc(sizeof(char) * strlen(role) + 1); + errno_t errorno = strncpy_s(user, strlen(role) + 1, role, strlen(role)); + securec_check(errorno, "\0", "\0"); + drop_objectstmt.behavior = stmt->behavior; + drop_objectstmt.type = T_DropOwnedStmt; + drop_objectstmt.roles = list_make1(makeString(user)); + + DropOwnedObjects(&drop_objectstmt); + list_free_deep(drop_objectstmt.roles); + } +``` + + + +检查是否有对象依赖于该角色,如果有则提示报错 + +```cpp +/* Check for pg_shdepend entries depending on this role */ + if (checkSharedDependencies(AuthIdRelationId, roleid, &detail, &detail_log)) + ereport(ERROR, + (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST), + errmsg("role \"%s\" cannot be dropped because some objects depend on it", role), + errdetail_internal("%s", detail), + errdetail_log("%s", detail_log))); +``` + + + +#### 授予和回收角色 + +SQL命令:GRANT/REVOKE + +通过函数GrantRole来实现 + +结构体GrantRoleStmt + + + +```cpp +typedef struct GrantRoleStmt { + NodeTag type; + List* granted_roles; /* 被授予或回收的角色集合 */ + List* grantee_roles; /* 从granted_roles中增加或删除的角色集合 */ + bool is_grant; /* true = GRANT授权, false = REVOKE回收 */ + bool admin_opt; /* 是否带有admin选项 */ + char* grantor; /* 授权者 */ + DropBehavior behavior; /* 是否级联回收角色 */ +} GrantRoleStmt; +``` + +大致流程: + +检查有权添加/删除角色成员 → 循环处理要添加/删除的角色 → 判断添加的成员是否已属于此角色 → 创建/修改pg_auth_member元组 → 新元组插入系统表 → 关闭系统表 → 结束 + +对角色的操作都大差不差,这里不过多介绍 + + + + + +### 身份认证简要逻辑 + +在了解了角色创建以及角色管理的流程后,让我们继续分析openGuass是如何对身份进行认证的。 + +openGauss的访问规则在配置文件HBA(Host-Based Authentication,主机认证)中。 + +格式为: + +```sql +hostssl DATABASE USER ADDRESS METHOD [OPTIONS] +``` + +第一个字段代表套接字方法 + +第二个字段代表被允许访问的数据库 + +第三个字段代表允许访问的用户 + +第四个字段代表允许访问的IP地址 + +第五个字段代表访问的认证方式 + +第六个字段对第五个字段认证信息的补充 + +自左向右,访问需求规则的优先级逐渐降低 + +源码如下: + +```c +/* src/include/lib/hba.h: 39-66 */ +typedef struct HbaLine { + int linenumber; /* 规则行号 */ + ConnType conntype; /* 连接套接字方法 */ + List* databases; /* 允许访问的数据库集合 */ + List* roles; /* 允许访问的用户组 */ + ... + char* hostname; /* 允许访问的IP地址 */ + UserAuth auth_method; /* 认证方法 */ + ... +} HbaLine; +``` + +HBA文件在系统管理员配置完成后存放在数据库服务侧。 + +当用户通过数据库用户发起的认证请求时,信息被存放在数据结构Port中 + +源码如下: + +```C +/* src/include/libpq/libpq-be.h: 98-204 */ + SockAddr laddr; /* 本地进程IP地址信息 */ + SockAddr raddr; /* 远程客户端进程IP地址信息 */ + char* remote_host; /* 远端主机名称字符串或IP地址 */ + char* remote_hostname; /* (可选)远程主机名称字符串或IP地址 */ + + /* 发送给后端的数据包信息,包括访问的数据库名称、用户名、配置参数 */ + char* database_name; + char* user_name; + char* cmdline_options; + List* guc_options; + + /* 认证规则信息 */ + HbaLine* hba; + ... + + /* SSL, 安全套接层 */ +#ifdef USE_SSL + SSL* ssl; + X509* peer; + char* peer_cn; + unsigned long count; +#endif + .... + + /* Kerberos 认证数据结构信息 */ +#ifdef ENABLE_GSS + char* krbsrvname; /* Kerberos服务进程名称 */ + gss_ctx_id_t gss_ctx; /* GSS(Generic Security Service, 通用安全服务)数据内容 */ + gss_cred_id_t gss_cred; /* GSS 凭证信息 */ + gss_name_t gss_name; /* GSS target name */ + gss_buffer_desc gss_outbuf; /* GSS token信息 */ +#endif +} Port; +``` + + + +在得到Port信息后,后台服务线程会根据前端传入的信息与**HbaLine**中记录的信息逐一比较,完成对应的身份识别。 + +在**check_hba函数**中,可以看到身份认证的核心逻辑代码: + +```c +/* src/common/backend/libpq/hba.cpp: 1523-1716 */ +/* */ +/* 扫描HBA文件,寻找匹配连接请求的规则项 */ +static void check_hba(hbaPort* port) +{ + ... + /* 获取目标用户的ID号 */ + roleid = get_role_oid(port->user_name, true); + ... + + /* */ + foreach (line, g_instance.libpq_cxt.comm_parsed_hba_lines) { + errno_t rc = memcpy_s(hba, sizeof(HbaLine), lfirst(line), sizeof(HbaLine)); + securec_check(rc, "\0", "\0"); + /* 检查连接类型,考虑时本地连接行为还是远程连接行为 */ + if (hba->conntype == ctLocal) { + /* 对于local套接字,仅允许初始安装用户本地登录 */ + if (roleid == INITIAL_USER_ID) { + uid_t uid = 0; + gid_t gid = 0; + /* 得到当前系统用户名,基于本地uid */ + if (getpeereid(port->sock, &uid, &gid) != 0) { + pfree_ext(hba); + ... + } + + /* + * For connections from another coordinator, we could not + * get the userid. This case may also exist for other cases, + * like tools. (Usually happed when login with -h localhost + * or 127.0.0.1) + */ + if ((long)uid == USER_NULL_MASK) { + continue; + } + /* 对照用户名,如果不匹配,设置为uaTrust不信任模式 */ + isUsernameSame= IsSysUsernameSameToDB(uid, port->user_name); + if (!isUsernameSame && hba->auth_method == uaTrust) { + hba->auth_method = get_default_auth_method(port->user_name); + } + } else if (hba->auth_method == uaTrust || hba->auth_method == uaPeer) { + /* 访问用户与本地系统用户不相匹配的场景,需要提供密码 */ + hba->auth_method = get_default_auth_method(port->user_name); + } + + if (!IS_AF_UNIX(port->raddr.addr.ss_family)) + continue; + } else { /* 如果时远程访问行为,则需要逐条判断炮廓认证方式在内的信息的正确性 */ + if (IS_AF_UNIX(port->raddr.addr.ss_family)) + continue; + + /* 检查SSL状态 */ +#ifdef USE_SSL + if (port->ssl != NULL) { + if (hba->conntype == ctHostNoSSL) + continue; + } else { + if (hba->conntype == ctHostSSL) + continue; + } +#else + if (hba->conntype == ctHostSSL) + continue; +#endif + + /* IP白名单校验 */ + switch (hba->ip_cmp_method) { + case ipCmpMask: + if (hba->hostname != NULL) { + if (!check_hostname(port, hba->hostname)) + continue; + } else { + if (!check_ip(&port->raddr, (struct sockaddr*)&hba->addr, (struct sockaddr*)&hba->mask)) + continue; + } + break; + case ipCmpAll: + break; + case ipCmpSameHost: + case ipCmpSameNet: + if (!check_same_host_or_net(&port->raddr, hba->ip_cmp_method)) + continue; + break; + default: + /* shouldn't get here, but deem it no-match if so */ + continue; + } + } /* != ctLocal */ + + /* 校验数据库信息和用户信息 */ + if (!check_db(port->database_name, port->user_name, roleid, hba->databases)) + continue; + + if (!check_role(port->user_name, roleid, hba->roles)) + continue; + + /* 记录相关信息以及继续执行校验 */ + if (hba->conntype != ctLocal) { + /* + * 通常,带有信任模式的远程连接请求是被禁止的 + * 但有一些例外场景: + * 1 由协调器和内部维护工具创建的连接请求 + * -- 信任模式被允许,在process_startup_options函数中实现了对内部组的检查 + * 2 本地环回连接 + * -- 密码被增强 + * 3 连接是由到协调器的组产生的非初始用户 + * -- 密码被增强 + */ + if (hba->auth_method == uaTrust) { + if (IsConnPortFromCoord(port) || u_sess->proc_cxt.IsInnerMaintenanceTools) { + /* 第一种情况,直接跳过 */ + } else if (IsLoopBackAddr(port)) { + /* 第二种情况, 本地环回连接, hba->remote_trust 设置为false */ + hba->auth_method = get_default_auth_method(port->user_name); + hba->remoteTrust = false; + } else if (roleid != INITIAL_USER_ID && IS_PGXC_COORDINATOR && is_cluster_internal_connection(port)) { + /* + * 第三种情况 + * 因为在成功认证之前,我们不能通过 pgxc_node 节点。 + * 我们能在重新连接时得到一个过度保守的内部组判断 + * 然而,这些情况不应该时问题,因为周期性地本地cm_agent连接很亏就会在共享内存中填充节点信息 + */ + hba->auth_method = get_default_auth_method(port->user_name); + hba->remoteTrust = false; + } else { + ConnAuthMethodCorrect = false; + pfree_ext(hba); + ereport(FATAL, + (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), + errmsg("Forbid remote connection with trust method!"))); + } + } + } + +#ifdef USE_IAM + /* 当使用带有SHA256/MD5/SM3加密时地iam用户去避免冲突时时,更改内部认证方式到IAM */ + if (isIAM && (hba->auth_method == uaSHA256 || hba->auth_method == uaMD5 || hba->auth_method == uaSM3)) { + hba->auth_method = uaIAM; + } +#endif +#if defined(ENABLE_GSS) || defined(ENABLE_SSPI) + /* 对于非初始化地用户, krb authentication是不被允许的, sha256 请求将会被通过. */ + if (hba->auth_method == uaGSS && roleid != INITIAL_USER_ID && IsConnFromApp()) { + hba->auth_method = get_default_auth_method(port->user_name); + } +#endif + isMatched = true; + break; + } + + /* 无匹配规则,直接拒绝当前连接请求 */ + if (!isMatched) { + hba->auth_method = uaImplicitReject; + } + copy_hba_line(hba); + port->hba = hba; + PG_END_ENSURE_ERROR_CLEANUP(hba_rwlock_cleanup, (Datum)0); + (void)pthread_rwlock_unlock(&hba_rwlock); +} +``` + + + + + +### 认证机制核心流程 + +在梳理了身份验证的基本逻辑之后,我们再去追溯一下认证机制的核心流程 + +大致流程如图: + + +![](pic_0\SF_1.png) + +#### 线程会话初始化入口 + +**执行函数:InitSession( )** + +代码位置: + +src\gausskernel\process\threadpool\threadpool_worker.cpp: 778-892 + +**传参:session** + +接受用户的身份信息session,切换至对应的空间地址mem + +```cpp +AutoContextSwitch memSwitch(session->mcxt_group->GetMemCxtGroup(MEMORY_CONTEXT_DEFAULT)); +``` + + + +更新工作版本Working version + +```cpp +t_thrd.proc->workingVersionNum = pg_atomic_read_u32(&WorkingGrandVersionNum); +``` + + + +初始化GUC(Grand Unified Configuration),并读取相关内容 + +```cpp +InitializeGUCOptions(); +read_nondefault_variables(); +``` + + + +安全地向用户端client发送错误信息 + +```cpp +t_thrd.postgres_cxt.whereToSendOutput = DestRemote; +// 思考在781行为什么设置暂不发送? +// t_thrd.postgres_cxt.whereToSendOutput = DestNone; +// 这里猜测是输出的状态信息在验证session的过程中会通过注入等方式泄露高权限用户的信息 +``` + + + +初始化端口号和连接 + +```cpp +if (!InitPort(session->proc_cxt.MyProcPort)) { + ... + } +``` + + + +再次切换工作版本号(从端口中得到的新版本号,why?) + +```cpp +t_thrd.proc->workingVersionNum = session->proc_cxt.MyProcPort->SessionVersionNum; +``` + + + +增加进程定义模式 + +```cpp + Reset_Pseudo_CurrentUserId(); + + SetProcessingMode(InitProcessing); + + SessionSetBackendOptions(); +``` + + + +允许SIGINT的系统调用去中止初始化程序 + +``` +gs_signal_setmask(&t_thrd.libpq_cxt.UnBlockSig, NULL); +``` + + + +初始化合法消息系统、状态、文件存储读取和流管理 + +```cpp + SharedInvalBackendInit(false, true); + pgstat_initialize_session(); + InitFileAccess(); + smgrinit(); +``` + + + +初始化openguass + +```cpp + char* dbname = session->proc_cxt.MyProcPort->database_name; + char* username = session->proc_cxt.MyProcPort->user_name; + t_thrd.proc_cxt.PostInit->SetDatabaseAndUser(dbname, InvalidOid, username); + t_thrd.proc_cxt.PostInit->InitSession(); +``` + + + +多节点情况下,执行操作 + +```cpp +#ifndef ENABLE_MULTIPLE_NODES + if (u_sess->proc_cxt.MyDatabaseId != InvalidOid && DB_IS_CMPT(B_FORMAT)) { + if (!u_sess->attr.attr_sql.dolphin) { + LoadDolphinIfNeeded(); + } else { + InitBSqlPluginHookIfNeeded(); + } + } else if (u_sess->proc_cxt.MyDatabaseId != InvalidOid && DB_IS_CMPT(A_FORMAT) && u_sess->attr.attr_sql.whale) { + InitASqlPluginHookIfNeeded(); + } +#endif +``` + + + +初始化哈希hash表 + +```cpp +if (IS_PGXC_COORDINATOR) { + init_set_params_htab(); + } +``` + + + +检查存储 + +```cpp +if (t_thrd.utils_cxt.gs_mp_inited && processMemInChunks > maxChunksPerProcess) { + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_LOGICAL_MEMORY), + errmsg("memory usage reach the max_dynamic_memory"), + errdetail("current memory usage is: %u MB, max_dynamic_memory is: %u MB", + (unsigned int)processMemInChunks << (chunkSizeInBits - BITS_IN_MB), + (unsigned int)maxChunksPerProcess << (chunkSizeInBits - BITS_IN_MB)))); + } + + ReadyForQuery((CommandDest)t_thrd.postgres_cxt.whereToSendOutput); +``` + + + +#### 连接认证总入口 + +**执行函数:PerformAuthentication( )** + +代码位置 + +src\common\backend\utils\init\postinit.cpp: 287-344 + +**传参:port** + +设置时效性,以防止意外bug + +```cpp +if (!enable_sig_alarm(u_sess->attr.attr_security.AuthenticationTimeout * 1000, true)) + ereport(FATAL, (errmsg("could not set timer for authorization timeout"))); +``` + + + +解锁SIGUSR2权限使触发SIGALARM去处理超时 + +```cpp +old_sigset = gs_signal_unblock_sigusr2(); +``` + + + +认证改变并恢复信号隐藏 + +```cpp +ClientAuthentication(port); +gs_signal_recover_mask(old_sigset); +``` + + + +认证结束,恢复环境量(禁用timeout, log) + +```cpp +if (!disable_sig_alarm(true)) + ereport(FATAL, (errmsg("could not disable timer for authorization timeout"))); + + if (u_sess->attr.attr_storage.Log_connections) { + if (AM_WAL_SENDER) + ereport(LOG, (errmsg("replication connection authorized: user=%s", port->user_name))); + else + ereport(LOG, (errmsg("connection authorized: user=%s database=%s", port->user_name, port->database_name))); + } + if (AM_WAL_DB_SENDER) { + Oid userId = get_role_oid(port->user_name, false); + CheckLogicalPremissions(userId); + } +``` + + + +更新用户登录计数器 + +```cpp +if (IsUnderPostmaster && !IsBootstrapProcessingMode() && !dummyStandbyMode) + InstrUpdateUserLogCounter(true); + + set_ps_display("startup", false); + + u_sess->ClientAuthInProgress = false; /* client_min_messages is active now */ + u_sess->misc_cxt.authentication_finished = true; +``` + + + +#### 读取并加载配置信息 + +**执行函数:load_hba( )** + +src\common\backend\libpq\hba.cpp: 1729-1829 + +读取配置文件: + +```cpp + file = AllocateFile(g_instance.attr.attr_common.HbaFileName, "r"); + linecxt = tokenize_file(g_instance.attr.attr_common.HbaFileName, file, &hba_lines, &hba_line_nums); + (void)FreeFile(file); +``` + + + +解析所有行: + +```cpp +hbacxt = AllocSetContextCreate(g_instance.instance_context, + "hba parser context", + ALLOCSET_DEFAULT_MINSIZE, + ALLOCSET_DEFAULT_INITSIZE, + ALLOCSET_DEFAULT_MAXSIZE, + SHARED_CONTEXT); + oldcxt = MemoryContextSwitchTo(hbacxt); + forboth(line, hba_lines, line_num, hba_line_nums) + { + HbaLine* newline = NULL; + + if ((newline = parse_hba_line((List*)lfirst(line), lfirst_int(line_num))) == NULL) { + MemoryContextReset(hbacxt); + new_parsed_lines = NIL; + ok = false; + continue; + } + + new_parsed_lines = lappend(new_parsed_lines, newline); + } +``` + + + + + +#### 查配置信息有效性并获取认证方式 + +**执行函数:hba_getauthmethod( )** + +src\common\backend\libpq\hba.cpp: 2142-2159 + +决定何种认证方式将被使用当访问数据库时 + +```cpp +void hba_getauthmethod(hbaPort* port) +{ +#ifdef ENABLE_MULTIPLE_NODES + if (IsDSorHaWalSender() ) { +#else + if (IsDSorHaWalSender() && (is_node_internal_connection(port) || AM_WAL_HADR_SENDER)) { +#endif + check_hba_replication(port); + } else { + check_hba(port); + } +} +``` + + + +#### 根据认证方式获取认证凭证并完成认证 + +**执行函数:crypt_verify( )** + +src\common\backend\libpq\crypt.cpp: 374-714 + +对加密方式进行确认 + +包括MD5、SHA256、SM3加密方式,这里会在另一篇文章 + +《openGauss安全认证机制之口令存储》里详细介绍 + +#### 完成认证并加载信息 + +**执行函数:InitUser( )** + +src\common\backend\utils\init\postinit.cpp 2252-2268 + +```cpp +void PostgresInitializer::InitUser() +{ + InitializeSessionUserId(m_username, false, m_useroid); + m_isSuperUser = superuser(); + u_sess->misc_cxt.CurrentUserName = u_sess->proc_cxt.MyProcPort->user_name; +#ifndef ENABLE_MULTIPLE_NODES + /* + * In opengauss, we allow twophasecleaner to connect to database as superusers + * for cleaning up temporary tables. During the cleanup of temporary tables, + * m_username and u_sess->proc_cxt.MyProcPort->user_name point to the same memory + * address. We have freed and reinitialized u_sess->proc_cxt.MyProcPort->user_name + * in function InitializeSessionUserId, and we need to initialize m_username here. + */ + if (u_sess->proc_cxt.IsInnerMaintenanceTools) + m_username = u_sess->proc_cxt.MyProcPort->user_name; +#endif +} +``` + + + + + + + +### 总结 + +不难看出,角色管理的各个方面与角色创建的操作几乎一模一样,这是遵循了对角色操作的规范性,避免因为代码的逻辑问题造成一些权限混乱,角色界限不清晰的问题。 + +身份认证作为数据库访问安全的核心,防范未授权访问,实现严谨的身份验证方法。 + +本文大致描述了身份认证的全过程,但并未展现认证细节。对于认证的各个方面,我们将在其他系列文章中进行一一展示。 \ No newline at end of file