如何在C++中完成认证并下载数据库服务器SSL证书
问题根因
你直接照搬HTTPS场景的OpenSSL连接逻辑拿不到数据库证书,核心原因非常明确:几乎所有数据库服务都不会在TCP连接建立后直接启动TLS握手。
- HTTPS 443端口默认TCP三次握手完成就立刻进入TLS协商流程,所以直接用
BIO_new_ssl_connect建连发起握手就能拿到证书 - 不管是MySQL、PostgreSQL还是SQL Server,都是先走明文协议交互:客户端先发启动包,服务端返回支持的能力列表,客户端明确发送SSL启用请求后,双方才会切换到TLS层。你上来直接发TLS Client Hello,服务端会把这识别为无效协议包直接断连,
BIO_do_connect()自然会报错。
可行实现方案
方案1:复用现有ODBC驱动能力获取证书(推荐,不用重复造轮子)
你本来就在用ODBC做连接查询,完全没必要自己从头实现数据库协议和TLS交互,驱动本身已经把这些逻辑封装好了:
- 先配置ODBC连接串,开启SSL连接选项,把SSL校验模式设为
verify-ca(不同驱动参数名略有差异,比如SQL Server对应参数是TrustServerCertificate=no),先给CA证书路径配置一个临时空文件路径 - 通过ODBC的连接属性接口注册证书回调:
- MySQL ODBC驱动可调用
SQLSetConnectAttr设置SSL证书回调,握手阶段驱动会把服务端返回的证书链直接传给回调函数 - PostgreSQL ODBC驱动支持注册
PGsslcertcallback,回调触发时就能拿到完整的服务端证书 - 部分ODBC驱动如果没有暴露证书回调,可以直接在驱动的SSL日志目录里找到缓存的服务端证书,或者临时用自签CA做本地代理抓包拿证书(仅限你有权限访问的服务,禁止做未授权操作)
- MySQL ODBC驱动可调用
- 回调里拿到X509证书对象后,直接用
PEM_write_bio_X509把证书存成PEM格式文件即可。身份认证逻辑也可以直接在回调里实现:校验证书的指纹、颁发者、主机名匹配规则是否符合预期,校验通过再返回0让驱动继续连接流程,校验失败直接返回错误断连即可。
方案2:手动实现协议前置交互+OpenSSL握手拿证书(无ODBC依赖场景)
如果需要脱离ODBC自主实现,先对应查阅你所用数据库的协议规范,走完SSL协商前的明文交互,再把socket交给OpenSSL处理:
- 建立普通TCP连接到数据库主机端口,不要直接挂载SSL BIO
- 按照对应数据库协议格式,发送SSL启用请求包:
- MySQL场景:发送长度为8的包,前3字节是包长度
0x00 0x00 0x00,第4字节是序列号0x01,后面4字节是SSL能力标记0x00 0x00 0x00 0x80 - PostgreSQL场景:发送8字节的SSL请求包,前4字节为大端序整数
80877103(固定SSL请求码)
- MySQL场景:发送长度为8的包,前3字节是包长度
- 等待服务端返回单字节响应:如果返回
S代表同意进入SSL握手,其他返回值代表服务端未启用SSL - 此时再把已经建立好的TCP socket绑定到OpenSSL的SSL BIO上,调用
SSL_connect()完成握手,就不会再出现之前的连接失败问题 - 握手完成后调用
SSL_get_peer_certificate()就能拿到服务端的证书对象,直接转成PEM格式存储到本地即可 - 身份认证环节:拿到证书后先校验证书有效期、主机名匹配度、证书指纹是否和预期一致,校验通过后再发送用户名密码做数据库登录,校验失败直接断开连接即可。
核心实现代码片段(以MySQL为例):
// 1. 建立普通TCP连接 int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 省略connect到数据库主机端口的标准socket逻辑... // 2. 发送MySQL SSL协商请求包 unsigned char ssl_req[] = {0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00}; send(sockfd, ssl_req, sizeof(ssl_req), 0); // 3. 读取服务端SSL协商响应 unsigned char resp; recv(sockfd, &resp, 1, 0); if (resp != 'S') { close(sockfd); throw std::runtime_error("target server does not support SSL"); } // 4. 将已连接的TCP socket绑定到OpenSSL SSL_CTX* ctx = SSL_CTX_new(TLS_client_method()); SSL* ssl = SSL_new(ctx); BIO* bio = BIO_new_socket(sockfd, BIO_NOCLOSE); SSL_set_bio(ssl, bio, bio); // 5. 完成TLS握手 int ret = SSL_connect(ssl); if (ret != 1) { // 省略握手错误处理逻辑 } // 6. 获取服务端证书并存储 X509* cert = SSL_get_peer_certificate(ssl); if (cert) { BIO* out = BIO_new_file("db_server_cert.pem", "w"); PEM_write_bio_X509(out, cert); BIO_free(out); // 此处执行证书校验逻辑:校验指纹、域名、有效期 // 校验通过后再发送账号密码完成数据库认证 X509_free(cert); } // 省略后续资源清理逻辑
注意:绝对不要跳过证书校验步骤就直接发送账号密码,否则会存在中间人攻击风险。拿到证书后建议先和运维侧核对证书指纹,确认是目标服务的合法证书后,再把它加到ODBC的信任CA列表里,开启强制SSL校验模式。
内容的提问来源于stack exchange,提问作者user17276231
相关产品推荐
相关产品推荐

