原生postgresql数据库仅支持使用opnessl进行ssl加密通信和gss加密通信,在此基础上开发第三种加密方式。
分析
postgresql数据库加密通信即客户端psql与数据库服务端的通信,为保证兼容性,加密应由libpq.so实现和控制,如果需要修改psql程序,则会造成其他客户端如JDBC、PGADMIN等客户端工具与postgresql数据库无法连接通信。可以考虑的方案有:
一、直接加密
libpq.so收发数据是通过文件fe-secure.c中的pqsecure_read和pqsecure_write两个函数进行的,而这两个函数又是分别通过pqsecure_raw_write和pqsecure_raw_read实现的,进入上述两个raw函数可以看到最底层的socket通信函数read和write:
进一步分析,pqsecure_read和pqsecure_write两个函数是所有加密和非加密通信的共同通道,而实际上,pgtls_read和pgtls_write底层也是使用的pqsecure_raw_write和pqsecure_raw_read实现的,所以,所有加密非加密共同的数据收发通道是pqsecure_raw_write和pqsecure_raw_read两个函数,如此一来,我们可以在pqsecure_raw_write和pqsecure_raw_read两个函数里对数据进行加解密,对于已加密的数据(ssl和gss)则会进行二次加密,如果不想进行二次加密,那么,自定义加解密位置可放在pqsecure_read和pqsecure_write两个函数里。
服务端的流程与此一致,但有一个细节需要注意,在ProcessStartupPacket函数里服务端首次接收客户端请求,将S或G或N发送给客户端的时候直接使用send函数,跳过了上述两层封装,做加解密的时候必须留意这个地方,否则会造成通信失败。
可能会遇到的问题有:
1、密钥如何协商?
2、数据对齐如何处理?
3、数据同步如何处理?
如上两个问题不解决则此方案不可行。
二、仿SSL协议做第三种加密协议进行加密
postgresql数据库通信使用的是postgresql协议,当前版本为V3.0,有完整的协议标准和消息格式,postgresql协议使用SSL协议进行加密的方式为:
1、客户端首先发送SSL请求,NEGOTIATE_SSL_CODE PG_PROTOCOL(1234,5679);
2、客户端根据是否支持SSL和回复单个字符'S'或'N';
3、客户端如果接收到的是'S',则开始建立SSL协议通道,即完成SSL握手过程;
4、SSL协议通道建立完毕则开始postgresql协议的认证和数据传输,也就是通过pgtls_read完成对postgresql协议明文数据的加解密,然后由pqsecure_raw_write将加密后的postgresql协议数据发送出去,接收则反过来。
实际过程和内容要比上述描述复杂的多,但核心内容大概如此,因此,我们可以考虑做自己的加密通道,流程仿照上述流程,协议标准需要自己定义。
同样需要面对的问题:
1、密钥如何协商?
2、数据对齐如何处理?
另外:
3、postgresql源码中将SSL的使用搀杂揉合进了postgresql协议的处理代码中,给增加自定义协议增加了较大难度。
三、修改postgresql协议
PostgreSQL协议是PostgreSQL数据库系统使用的客户端与服务器之间的通信协议,它定义了两者之间如何交换信息以执行查询、获取结果和其他数据库操作。此协议是基于请求-响应模型的,支持多种操作,包括认证、查询执行、数据传输等。
PostgreSQL协议的消息格式设计得相当简单且灵活,它基于一系列前后端之间交换的消息。以下是协议的基本格式和关键组件:
消息结构
每个消息都遵循基本的格式:
消息长度(Message Length):一个4字节的大端表示的整数,表示消息体(不包括长度自身)的字节数。
消息类型(Message Type):一个1字节的整数,代表消息的类型。例如,'Q' 表示查询请求(Query),'R' 表示行数据(DataRow)等。
消息体(Message Body):根据消息类型不同,包含不同的数据。例如,查询请求的消息体就是SQL查询字符串,而行数据消息体则包含实际的数据行值。
关键消息类型
Authentication:用于认证过程,服务器发送认证请求,客户端回复认证信息。
Query:客户端发送SQL查询给服务器。
Parse:客户端发送SQL语句给服务器进行解析,并指定参数数据类型,但不立即执行。
Bind:将之前解析的SQL语句与具体的参数值绑定,准备执行。
Execute:执行之前绑定的SQL语句。
Sync:客户端用来同步,确保之前的所有消息已被处理。
Close:关闭之前打开的游标或预处理语句。
Terminate:客户端或服务器用来结束会话。
根据上面对postgresql协议的介绍,我们可以考虑对postgresql协议进行扩展,新加密钥协商过程。
问题:
1、需要对postgresql协议有较深入了解。
综上,如果想要对postgresql进行自定义通信加密开发,首先要面对的是密钥协商、数据对齐等问题,然后可能要详细分析postgresql协议和实现、postgresql中SSL的使用等等。
针对上面的问题,可以考虑:
1、可以考虑预分配、预主对称密钥、公钥加密传输、密钥交换算法等;
2、PKCS7
3、流加密、规定消息格式
实现
按照上述分析的第二种方式,实现一种与使用SSL协议类似的加密通信方式,具体如下:
1、在结构体internalPQconninfoOption PQconninfoOptions中添加客户端的控制选项,增加该项就可以通过环境变量PGUKEYMODE或psql参数ukeymode赋值为require或disable启用或禁止自定义通信加密功能:
{"ukeymode", "PGUKEYMODE", DefaultUKEYMode, NULL,
"UKEY-Mode", "", 8, /* sizeof("require") == 8 */
offsetof(struct pg_conn, ukeymode)},
此处定义为UKEY是因为最初是想用USBKEY完成加解密的,但因设备不给力暂时使用gmssl库代替。
2、pqsecure_write和pqsecure_read函数增加ukey读写:
3、secure_write和secure_read函数:
因为采用的是SM4-CBC模式加解密,要求数据块为整块数据(16字节倍数),而实际信中数据长度可能为任何值,在此需要填充,又因为TCP传输可能拆包或粘包,所以一次recv的数据不一定是对端一次加密的完整的一组数据,所以使用了封装:加密一组数据后在发送前首先发送4字节长度数据,对端首先接收4字节的长度值,然后根据长度接收一整包加密数据再解密。
上面的ukey读写函数有点粗糙,仅为验证方案可行性,实际使用中需要详细处理数据收发和加解密错误。
4、数据填充:PKCS7
通信加密采用的是SM4加密算法,使用的库是libgmssl3.so。
4、密钥交换:会话加密算法是SM4,会话密钥如何分发?我使用的是客户端与服务端分别临时生成一组非对称(SM2)密钥对,分别将自己的公钥发送给对端,然后各自将各自通过随机数产生的会话密钥用对端公钥加密后发送给对端,对端解密后存储,完成密钥交换,加解密时分别使用自己生成的密钥加密要发送的数据,使用对端的密钥解密接收到的数据:
int
secure_exchange_key(Port *port)
{
#if 1
ssize_t n = 0;
size_t plainlen = 0;
SM2_CIPHERTEXT ciphertext;
if(!pg_strong_random((void *)port->server_key, 32)){
ereport(DEBUG2,
(errmsg("Description The server failed to generate a symmetric key\n")));
return -1;
}
if (sm2_key_generate(&port->server_sm2_key) != 1) {
ereport(DEBUG2,
(errmsg("Description Failed to generate the SERVER SM2 key\n")));
return -1;
}
n = secure_read(port, (void *)&port->client_sm2_key.public_key, sizeof(port->client_sm2_key.public_key));
if(n != sizeof(port->client_sm2_key.public_key))
{
ereport(DEBUG2,
(errmsg("Failed to receive the public key of the client. Procedure\n")));
return -1;
}
n = secure_write(port, (void *)&port->server_sm2_key.public_key, sizeof(port->server_sm2_key.public_key));
if(n != sizeof(port->server_sm2_key.public_key)){
ereport(DEBUG2,
(errmsg("Failed to send the server public key. Procedure\n")));
return -1;
}
if (sm2_do_encrypt(&port->client_sm2_key, port->server_key, sizeof(port->server_key), &ciphertext) != 1){
ereport(DEBUG2,
(errmsg("Failed to encrypt the server symmetric key using the client public key\n")));
return -1;
}
n = secure_write(port, (void *)&ciphertext, sizeof(ciphertext));
if(n != sizeof(ciphertext)){
ereport(DEBUG2,
(errmsg("Failed to send the server key encrypted using the client public key. Procedure\n")));
return -1;
}
if(secure_read(port, (void *)&ciphertext, sizeof(ciphertext)) != sizeof(ciphertext))
{
ereport(DEBUG2,
(errmsg("Description Failed to receive the symmetric key ciphertext from the client\n")));
return -1;
}
if (sm2_do_decrypt(&port->server_sm2_key, &ciphertext, port->client_key, &plainlen) != 1)
{
ereport(DEBUG2,
(errmsg("Description Failed to decrypt the server symmetric key\n")));
return -1;
}
if (plainlen != sizeof(port->client_key)){
ereport(DEBUG2,
(errmsg("Description Failed to decrypt the client symmetric key length")));
return -1;
}
#endif
//memset(port->client_key, 0x5A, 32);
//memset(port->server_key, 0x5A, 32);
port->ukey_in_use = true;
return 0;
}
PostgresPollingStatusType
pq_exchange_key(PGconn *conn)
{
#if 1
ssize_t n = 0;
size_t plainlen = 0;
SM2_CIPHERTEXT ciphertext;
if(!pg_strong_random((void *)conn->client_key, 32)){
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext(
"Description The client failed to generate a symmetric key\n"));
return PGRES_POLLING_FAILED;
}
if (sm2_key_generate(&conn->client_sm2_key) != 1) {
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext(
"Description Failed to generate the SM2 key\n"));
return PGRES_POLLING_FAILED;
}
n = pqsecure_write(conn, (void *)&conn->client_sm2_key.public_key, sizeof(conn->client_sm2_key.public_key));
if(n != sizeof(conn->client_sm2_key.public_key)){
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext(
"Failed to send the client public key. Procedure\n"));
return PGRES_POLLING_FAILED;
}
retry1:
n = pqsecure_read(conn, (void *)&conn->server_sm2_key.public_key, sizeof(conn->server_sm2_key.public_key));
if(n <= 0)
goto retry1;
else if (n != sizeof(conn->server_sm2_key.public_key))
{
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext(
"Failed to receive the public key of the server. Procedure\n"));
return PGRES_POLLING_FAILED;
}
retry2:
n = pqsecure_read(conn, (void *)&ciphertext, sizeof(ciphertext));
if(n <= 0)
goto retry2;
else if (n != sizeof(ciphertext))
{
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext(
"Description Failed to receive the symmetric key ciphertext from the server\n"));
return PGRES_POLLING_FAILED;
}
if (sm2_do_decrypt(&conn->client_sm2_key, &ciphertext, conn->server_key, &plainlen) != 1)
{
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext(
"Description Failed to decrypt the server symmetric key\n"));
return PGRES_POLLING_FAILED;
}
if (sm2_do_encrypt(&conn->server_sm2_key, conn->client_key, sizeof(conn->client_key), &ciphertext) != 1){
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext(
"Failed to encrypt the client symmetric key using the server public key\n"));
return PGRES_POLLING_FAILED;
}
n = pqsecure_write(conn, (void *)&ciphertext, sizeof(ciphertext));
if(n != sizeof(ciphertext)){
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext(
"Failed to send the client key encrypted using the server public key. Procedure\n"));
return PGRES_POLLING_FAILED;
}
if (plainlen != sizeof(conn->server_key)){
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext(
"Description Failed to decrypt the server symmetric key length"));
return PGRES_POLLING_FAILED;
}
#endif
//memset(conn->client_key, 0x5A, 32);
//memset(conn->server_key, 0x5A, 32);
conn->ukey_in_use = true;
return PGRES_POLLING_OK;
}
客户端与服务端完成密钥交换后分别将ukey_in_use置为true,后面的通信数据就是加密后的数据了。
5、自定义加密请求:以上工作仅仅完成了加解密的工作,何时?如何?由谁去调用密钥交换函数开启自定义加密通道呢?客户端设置环境变量PGUKEYMODE=require请求UKEY加密,服务端根据是否支持该功能返回字符'U'或'N'响应支持或不支持(SSL协议返回的是'S'或'N'),此时如果与SSL协议冲突导致协议协商失败可先设置PGSSLMODE=disable:
1)根据PGUKEYMODE是否设置为require决定是否尝试UKEY加密请求:
2)如果设置启用UKEY加密请求,优先发送UKEY加密请求而忽略SSL设置:
3)增加UKEY加密请求代码:
4)增加对UKEY加密请求响应的处理:
case CONNECTION_UKEY_STARTUP:
{
#ifdef USE_GMSSL
PostgresPollingStatusType pollres;
/*
* On first time through, get the postmaster's response to our
* SSL negotiation packet.
*/
if (!conn->ukey_in_use)
{
/*
* We use pqReadData here since it has the logic to
* distinguish no-data-yet from connection closure. Since
* conn->ssl isn't set, a plain recv() will occur.
*/
char UKEYok;
int rdresult;
rdresult = pqReadData(conn);
if (rdresult < 0)
{
/* errorMessage is already filled in */
goto error_return;
}
if (rdresult == 0)
{
/* caller failed to wait for data */
return PGRES_POLLING_READING;
}
if (pqGetc(&UKEYok, conn) < 0)
{
/* should not happen really */
return PGRES_POLLING_READING;
}
if (UKEYok == 'U')
{
/* mark byte consumed */
conn->inStart = conn->inCursor;
/* Set up global SSL state if required */
//if (pqsecure_initialize(conn) != 0)
// goto error_return;
}
else if (UKEYok == 'N')
{
/* mark byte consumed */
conn->inStart = conn->inCursor;
/* OK to do without SSL? */
//if (conn->ukeymode[0] == 'r' || /* "require" */
// conn->ukeymode[0] == 'v') /* "verify-ca" or
// * "verify-full" */
{
/* Require SSL, but server does not want it */
appendPQExpBufferStr(&conn->errorMessage,
libpq_gettext("server does not support UKEY, but UKEY was required\n"));
goto error_return;
}
/* Otherwise, proceed with normal startup */
conn->allow_ukey_try = false;
conn->status = CONNECTION_MADE;
return PGRES_POLLING_WRITING;
}
else if (UKEYok == 'E')
{
/*
* Server failure of some sort, such as failure to
* fork a backend process. We need to process and
* report the error message, which might be formatted
* according to either protocol 2 or protocol 3.
* Rather than duplicate the code for that, we flip
* into AWAITING_RESPONSE state and let the code there
* deal with it. Note we have *not* consumed the "E"
* byte here.
*/
conn->status = CONNECTION_AWAITING_RESPONSE;
goto keep_going;
}
else
{
appendPQExpBuffer(&conn->errorMessage,
libpq_gettext("received invalid response to UKEY negotiation: %c\n"),
UKEYok);
goto error_return;
}
}
/*
* Begin or continue the SSL negotiation process.
*/
pollres = pq_exchange_key(conn);
if (pollres == PGRES_POLLING_OK)
{
/* SSL handshake done, ready to send startup packet */
conn->status = CONNECTION_MADE;
return PGRES_POLLING_WRITING;
}
if (pollres == PGRES_POLLING_FAILED)
{
/* Else it's a hard failure */
goto error_return;
}
/* Else, return POLLING_READING or POLLING_WRITING status */
return pollres;
#else /* !USE_SSL */
/* can't get here */
goto error_return;
#endif /* USE_SSL */
}
如果收到服务端响应为'U',则调用密钥交换函数,密钥交换成功则置ukey_in_use为true,即开启通信加密。
6、服务端响应加密请求:
1)对ukey加密请求的响应:
如果服务端支持ukey加密则在收到客户端的UKEY加密请求NEGOTIATE_UKEY_CODE后回复'U',并调用密钥交换函数等待密钥交换,密钥交换成功后置服务端的ukey_in_use为true,开启ukey加密通信。
7、配置pg_hba.conf文件:如果想要通过配置选择是否采用UKEY通信加密,可以配置pg_hba.conf文件的第一列通信类型为hostenc或hostnoenc:
1)pg_hba.conf文件分析:
读取到pg_hba.conf文件配置为hostenc或hostnoenc时做此处理
2)检查通信配置:
当程序流程走到函数check_hba里时,加密或不加密的通信通道已建立完毕,此时检查实际建立的加密通道是否与配置相符,如果不相符则拒绝连接。此处代码进入continue后就会被拒绝连接。
注意:SSL通信默认是尝试请求SSL连接,如果被服务端拒绝会自动进行非加密的连接请求,实际测试UEKY请求被拒绝后不能自动进行非加密请求,另外UKEY与SSL功能同时存在时还有些冲突,需要后续优化处理。
到此,postgresql数据库自定义加密通信框架已基本完成。