首先我们知道在PKI体系中,证书的生命周期如下所示,
对于一个可信任的 CA 机构颁发的有效证书,在证书到期之前,只要 CA 没有把其吊销,那么这个证书就是有效可信任的。有时,由于某些特殊原因(比如私钥泄漏,证书信息有误,CA 有漏洞被黑客利用,颁发了其他域名的证书等等),需要吊销某些证书。那浏览器或者客户端如何知道当前使用的证书已经被吊销了呢?为解决对CA证书有效性方面的查询,PKI引入了CRL(Certificate Revocation List)和OCSP(Online Certificate Status Protocol) 技术。前者需要用户按时定期下载,可用于脱机使用;后者则可实时在线查询。
接下来我们来介绍下OCSP技术。(至于CRL这次我们先不关注,以后有机会再详聊 (・ω≦))
OCSP(Online Certificate Status Protocol)即在线证书状态协议,是一个互联网协议,用于获取符合X.509标准的数字证书的状态。OCSP是维护服务器和其它网络资源安全性的两种普遍模式之一。OCSP协议的产生是用于在公钥基础设施(PKI)体系中替代证书吊销列表(CRL)来查询数字证书的状态,当用户试图访问一个服务器时,在线证书状态协议发送一个对于证书状态信息的请求。服务器回复一个“有效”、“过期”或“未知”的响应。
Alice和Bob使用Ivan颁发的数字证书。该场景中Ivan是数字证书认证中心CA机构;
OCSP是一种相对简单的请求/响应协议,以C/S模式实现,并没有明确协议所使用的传输机制,也没有明确OCSP系统的结构。因此,建立一个OCSP系统为用户提供OCSP服务的实现模式不是唯一的。
OCSP客户端通常称为OCSP请求者,而OCSP服务器通常称为OCSP响应器。OCSP协议工作方式如下,
(有关协议更多详情可参考:http://www.cnpaf.net/Class/Rfcen/200502/3610.html)
3.1、OCSP请求消息描述
OCSP请求包括的信息主要有:协议版本号、请求者标识、目标证书标识和可选的扩展项等。
当OCSP响应器收到请求之后,它会检查请求的内容:
若检查通过,返回一个确定的回复,OCSP响应端回复的加密消息中包含证书的状态可以是:
若任何一个先决条件没有满足,那么OCSP响应器将产生一个错误信息,错误码可能包含以下内容:
3.2、OCSP响应消息描述
OCSP响应可能是一个确定的响应或一个标识异常情况的出错信息。对每个确定的响应,响应器必须签名,对出错信息不必签名。确定的响应中包含协议版本号、响应器名、对每一张待查询证书的回复、可选的扩展项、签名算法对象标识和签名值。
所有确定的响应消息必须经过数字签名以保证响应是源于可信任方并且传输过程中没有被改动。用来签名响应消息的密钥必须是下列中的一个:
当接收一个签名响应时,OCSP客户方必须检测一下几项:
只有当上述所有条件都得到确认之后,客户方才可接收有响应器传送过来的证书状态信息。
openssl在crypto/ocsp目录实现了ocsp模块,包括客户端和服务端各种函数:
函数名 | 功能 |
---|---|
i2d_OCSP_REQUEST | 将OCSP_REQUEST数据结构DER编码 |
d2i_OCSP_RESPONSE |
将DER编码的数据转换为OCSP_RESPONSE数据结构 |
OCSP_request_add0_id |
本函数用于往请求消息中添加一个证书ID;他将一个OCSP_CERTID信息存入OCSP_REQUEST结构,返回内部生成的OCSP_ONEREQ指针 |
OCSP_request_add1_nonce | 添加nonce扩展项,val和len表明了nonce值 |
OCSP_response_status | 本函数获取OCSP响应状态 |
OCSP_response_get1_basic | 本函数从响应数据结构中获取OCSP_BASICERESP信息 |
OCSP_resp_get0 | 给定单个响应的序号,从堆栈中取出; |
OCSP_cert_to_id | 根据摘要算法、持有者证书和颁发这证书生成OCSP_CERTID数据结构; |
OCSP_id_cmp | 比较OCSP_CERTID,本函数比较所有项,包括证书序列号; |
OCSP_check_nonce | 检测nonce,用于防止重放攻击;检查请求和响应的nonce扩展项,看他们是否相同; |
OCSP_single_get0_status | 获取单个证书状态,返回值为其状态,ocsp.h中定义; |
OCSP_check_validity | 时间检查计算,合法返回1,thisupd为本次更新时间,nextupd为下次更新时间; |
实现流程:(下面只列出了实际应用项目中的主要代码段 o( ̄▽ ̄)d )
1>、创建和发送 OCSP 请求,首先需要加载颁发者证书和主题证书。
颁发者证书 x509_issuer ——> 例如: pkm
主题证书 x509_subject ——> 例如: 待验证的证书
2>、为了创建请求,我们需要为主题证书创建一个证书 ID,以便 CA 知道我们询问的是哪个证书。
openssl提供了以下接口来实现:
OCSP_CERTID *reqid;
const EVP_MD *md = EVP_sm3(); //返回返回一个sm3的EVP_MD的结构
......
reqid = OCSP_cert_to_id(md, x509_subject, x509_issuer);
//Note. md为创建ID时所用的hash算法,
//若传入NULL, 该方法内部将默认使用SHA1,这里我们采用sm3算法。
//设置的算法需要与ocsp服务端的响应证书ID中设置的算法一致,
//否则在进行请求中证书id与响应中证书id匹配时将失败
3>、然后创建一个请求并向其添加证书 ID。
OCSP_REQUEST *req = NULL;
OCSP_ONEREQ *onereq = NULL;
req = OCSP_REQUEST_new(); //创建请求
//向请求中添加证书ID
onereq = OCSP_request_add0_id(req, reqid);
4>、在请求中添加一个 nonce 可防止重放攻击,但并非所有 CA 都处理 nonce。
//openssl中提供了如下接口,设置nonce.
OCSP_request_add1_nonce(...);
//注.我们的实际应用中由于CA未处理nonce,所以这里我们也不设置nonce.
5>、要将请求提交给 CA 进行验证,我们需要从主题证书中提取 OCSP URI。
//由于实际应用中,我们的证书中并没有ocsp URI,
//因此,这里OCSP URI通过外部提供,而不是从证书中解析提取。
6、要提交请求,我们会将请求发送到 OCSP URI。(根据OCSP协议规定,所有请求消息的内容都以ASN.1语言描述,采用DER编码)
unsigned char *reqBuf = NULL;
unsigned int reqBufLen = 0;
......
reqBufLen = i2d_OCSP_REQUEST(req, &reqBuf); //将请求结构数据转为DER编码的数据
//至于如何将请求发送到OCSP URI,这与使用的Http通信技术有关,
//这里我们是利用libcurl库进行通信,具体实现就不说了~
//......
7、获取响应数据(根据OCSP协议规定,响应消息的内容是DER编码的数据)。
OCSP_RESPONSE *resp = NULL;
......
//当通过http通信取得服务端回复的响应数据后,将其解析为OCSP_RESPONSE
resp = d2i_OCSP_RESPONSE(NULL, &ptr, respLen);
if (NULL == resp)
{
//获取响应数据resp失败
//TODO.错误处理......
}
8、该响应包含状态信息(成功/失败)。我们可以将状态显示为一个字符串。
int responder_status = OCSP_response_status(resp);
if (OCSP_RESPONSE_STATUS_SUCCESSFUL != responder_status)
{
//响应不成功
//TODO.错误处理......
}
OCSP responder的状态有以下几种情况:
# define OCSP_RESPONSE_STATUS_SUCCESSFUL 0 //成功
# define OCSP_RESPONSE_STATUS_MALFORMEDREQUEST 1 //格式错误的请求
# define OCSP_RESPONSE_STATUS_INTERNALERROR 2 //内部错误
# define OCSP_RESPONSE_STATUS_TRYLATER 3 //请稍后再试
# define OCSP_RESPONSE_STATUS_SIGREQUIRED 5 //请求需要签名
# define OCSP_RESPONSE_STATUS_UNAUTHORIZED 6 //未授权的
9、接下来,我们需要知道回复的详细信息,以确定回复是否符合我们的要求。
主要从以下几项进行确认:
OCSP_BASICRESP *basicresp = NULL;
OCSP_SINGLERESP *single = NULL;
OCSP_CERTID *respid = NULL;
ECCPUBLICKEYBLOB pubKeyBlob = { 0, { 0 }, { 0 } };
unsigned char *outBytes = NULL;
unsigned int len = 0;
ECCSIGNATUREBLOB *signature = NULL;
//TODO. 通过Http连接取得OCSP响应数据
//......
//......
basicresp = OCSP_response_get1_basic(resp); //从响应数据结构中获取OCSP_BASICERESP信息
if (NULL == basicresp)
{
//获取OCSP_BASICERESP信息失败
//TODO.错误处理......
}
//openssl中提供了如下方法进行nonce检查, 这里我们不进行nonce检查。
//if (1 == OCSP_check_nonce(req, basicresp)) //检测nonce,用于防止重放攻击;检查请求和响应的nonce扩展项,看他们是否相同;
//{
// //请求和响应的nonce扩展项不一致
// //TODO.错误处理......
//}
//比较请求中证书id与响应中证书id是否一致
single = OCSP_resp_get0(basicresp, 0);
//对于较低版本的openssl(如openssl 1.0.2.d)中,可直接通过single->certId方式获取证书ID;
//高版本中,例如gmssl中通过OCSP_SINGLERESP_get0_id接口获取单个响应中的证书ID
//respid = OCSP_SINGLERESP_get0_id(single);
//if (0 != OCSP_id_cmp(reqid, respid))
if (0 != OCSP_id_cmp(reqid, single->certId))
{
//请求中证书id与响应中证书id不一致
//TODO.错误处理......
}
//-----签名验证-----
//取公钥
respCert = SKM_sk_value(X509, basicresp->certs, 0); //从响应数据中提取证书
ret = getPubKeyFromX509(respCert, &pubKeyBlob, TRUE); //从x509证书中取得公钥
if (0 != ret)
{
//公钥获取失败
//TODO.错误处理......
}
//取得签名原文
len = i2d_OCSP_RESPDATA(basicresp->tbsResponseData, &outBytes);
if (0 == len)
{
//签名原文提取失败
//TODO.错误处理......
}
//取得签名值
//高版本openssl中,可通过OCSP_resp_get0_signature接口取得ASN1_OCTET_STRING类型的签名值结构
signedLen = basicresp->signature->length;
ptrSigned = basicresp->signature->data;
//验签
//注. 对于实际应用中,需根据ocsp服务端填充的签名值结构,以及验签处理接口的要求,对签名值、原文等进行处理。
//处理完成,调用我们对应的签名验签接口,验证响应签名,例如我们这里的验签接口为VerifySignedData
ulRet = VerifySignedData(...);
if (SAR_OK != ulRet)
{
//验签失败
//TODO.错误处理......
}
10、然后从基本响应中提取证书的状态信息。
int status;
ASN1_GENERALIZEDTIME *rev, *thisupd, *nextupd;
......
......
//获取单个证书状态,返回值为其状态,ocsp.h中定义
status = OCSP_single_get0_status(single, &reason, &rev, &thisupd, &nextupd);
证书的状态有三种:
# define V_OCSP_CERTSTATUS_GOOD 0
# define V_OCSP_CERTSTATUS_REVOKED 1
# define V_OCSP_CERTSTATUS_UNKNOWN 2
11、最后检查有效性。时间检查。
int nsec = 5 * 60, maxage = -1;
......
......
//时间检查计算,合法返回1,thisupd为本次更新时间,nextupd为下次更新时间
if (1 != OCSP_check_validity(thisupd, nextupd, nsec, maxage))
{
//ocsp检查不合法。
//TODO:错误处理......
}
哦了,港完收工(((((((((((っ•ω•)っ Σ(σ`•ω•´)σ 起飞!