如何通过OpenSSL API从SSL_CTX的证书链中提取叶子证书?
问题描述
我在C程序中使用OpenSSL API与远程服务器建立双向TLS(Mutual-TLS)连接。建立连接时,客户端使用用户提供的任意X509 PEM证书(自签名证书、CA签发的叶子证书或链式证书均可),通过
SSL_CTX_use_certificate_chain_file()将其加载到SSL_CTX中,该函数支持加载单证书或证书链。我的目标是在终端打印叶子证书的元数据,示例输出如下:
% my_tls_client -key foo.key -cert foo.crt myserver.com:2566 Attempting to connect to server myserver.com port:2566 Using host certificate as follows: serial: aa:bb:cc:dd:ee subject: foo... valid from: DATE1 valid until: DATE2 WARNING: The provided certificate is about to expire in X days ! Connected to server successfully!为此,我需要先从SSL_CTX的X509_STORE中识别并获取叶子(主机)证书。请问是否存在可从证书链中获取叶子证书的OpenSSL API?我了解到一种方法建议使用
SSL_CTX_get0_certificate(),但未在OpenSSL文档中找到该方法的相关参考;另一个APIX509_STORE_CTX_get_current_cert()似乎也无法专门获取叶子证书。
解决方案
1. 直接使用SSL_CTX_get0_certificate()获取叶子证书
SSL_CTX_get0_certificate()是OpenSSL 1.0.2及以上版本的合法API,专门用于获取SSL_CTX中设置的叶子证书(证书链的第一个证书)。你没找到文档可能是因为查看的版本不匹配,该函数在对应版本的官方文档中是存在的。
调用示例:
X509 *leaf_cert = SSL_CTX_get0_certificate(ctx); if (leaf_cert == NULL) { fprintf(stderr, "Failed to get leaf certificate from SSL_CTX\n"); return -1; }
2. 从已建立的SSL连接对象获取叶子证书
如果已经完成TLS连接建立,也可以直接从SSL对象中提取当前使用的叶子证书,双向TLS场景下同样适用:
X509 *leaf_cert = SSL_get0_certificate(ssl); if (leaf_cert == NULL) { fprintf(stderr, "Failed to get leaf certificate from SSL object\n"); return -1; }
3. 手动遍历证书链(备选方案)
如果需要确认证书链中的叶子证书(比如处理特殊的链顺序),可以通过以下逻辑判断:
- 自签名证书:subject与issuer字段完全相同
- 非自签名叶子证书:通常是证书链的第一个证书(
SSL_CTX_use_certificate_chain_file会按文件顺序加载,叶子在前)
遍历示例代码:
STACK_OF(X509) *chain = NULL; SSL_CTX_get0_chain_certs(ctx, &chain); if (chain == NULL) { fprintf(stderr, "Failed to get certificate chain\n"); return -1; } X509 *leaf_cert = NULL; for (int i = 0; i < sk_X509_num(chain); i++) { X509 *cert = sk_X509_value(chain, i); // 判断是否为自签名证书 if (X509_NAME_cmp(X509_get_subject_name(cert), X509_get_issuer_name(cert)) == 0) { leaf_cert = cert; break; } // 非自签名证书取链中第一个 if (i == 0) { leaf_cert = cert; break; } }
4. 打印叶子证书元数据示例
拿到叶子证书后,可通过OpenSSL API提取并打印所需元数据:
// 打印证书序列号 ASN1_INTEGER *serial = X509_get_serialNumber(leaf_cert); char serial_buf[256]; i2a_ASN1_INTEGER(NULL, serial, serial_buf, sizeof(serial_buf)); printf(" serial: %s\n", serial_buf); // 打印证书subject字段 char subject_buf[1024]; X509_NAME_oneline(X509_get_subject_name(leaf_cert), subject_buf, sizeof(subject_buf)); printf(" subject: %s\n", subject_buf); // 打印有效期 char nb_buf[64], na_buf[64]; ASN1_TIME *not_before = X509_get_notBefore(leaf_cert); ASN1_TIME *not_after = X509_get_notAfter(leaf_cert); ASN1_TIME_to_string(not_before, nb_buf, sizeof(nb_buf)); ASN1_TIME_to_string(not_after, na_buf, sizeof(na_buf)); printf(" valid from: %s\n", nb_buf); printf(" valid until: %s\n", na_buf); // 计算并提示过期预警 time_t now = time(NULL); time_t not_after_t = ASN1_TIME_to_time_t(not_after); long days_left = (not_after_t - now) / (60 * 60 * 24); if (days_left < 30 && days_left >= 0) { printf(" WARNING: The provided certificate is about to expire in %ld days !\n", days_left); } else if (days_left < 0) { printf(" WARNING: The provided certificate has expired !\n"); }
注:ASN1_TIME_to_string和ASN1_TIME_to_time_t是OpenSSL 1.1.0及以上版本提供的函数,低版本需要手动解析ASN1_TIME结构。
内容的提问来源于stack exchange,提问作者Anirban

