首先我们知道在PKI体系中,证书的生命周期如下所示,
对于一个可信任的 CA 机构颁发的有效证书,在证书到期之前,只要 CA 没有把其吊销,那么这个证书就是有效可信任的。有时,由于某些特殊原因(比如私钥泄漏,证书信息有误,CA 有漏洞被黑客利用,颁发了其他域名的证书等等),需要吊销某些证书。那浏览器或者客户端如何知道当前使用的证书已经被吊销了呢?为解决对CA证书有效性方面的查询,PKI引入了CRL(Certificate Revocation List)和OCSP(Online Certificate Status Protocol) 技术。前者需要用户按时定期下载,可用于脱机使用;后者则可实时在线查询。
接下来我们来介绍下OCSP技术。(至于CRL这次我们先不关注,以后有机会再详聊 (・ω≦))
1、OCSP的定义
OCSP(Online Certificate Status Protocol)即在线证书状态协议,是一个互联网协议,用于获取符合X.509标准的数字证书的状态。OCSP是维护服务器和其它网络资源安全性的两种普遍模式之一。OCSP协议的产生是用于在公钥基础设施(PKI)体系中替代证书吊销列表(CRL)来查询数字证书的状态,当用户试图访问一个服务器时,在线证书状态协议发送一个对于证书状态信息的请求。服务器回复一个“有效”、“过期”或“未知”的响应。
2、基本PKI应用场景
Alice和Bob使用Ivan颁发的数字证书。该场景中Ivan是数字证书认证中心CA机构;
- ① Alice向Bob发送其由Ivan颁发的数字证书,并发出请求建立连接的申请;
- ② Bob担心Alice的私钥已经泄露,因此向Ivan发送‘OCSP request’消息并包含Alice的数字证书序列号;
- ③ Ivan的OCSP响应端从Bob发送的消息中获取数字证书的序列号,并在CA数据库中查找该数字证书的状态;
- ④ Ivan向Bob发送由其私钥加密的消息‘OCSP response’,并包含证书状态正常的信息;
- ⑤ 由于Bob事先已经安装了Ivan的数字证书,因此Bob使用Ivan的公钥解密消息并获取到Alice的数字证书状态信息;
- ⑥ Bob决定与Alice进行通信。
3、协议分析
OCSP是一种相对简单的请求/响应协议,以C/S模式实现,并没有明确协议所使用的传输机制,也没有明确OCSP系统的结构。因此,建立一个OCSP系统为用户提供OCSP服务的实现模式不是唯一的。
OCSP客户端通常称为OCSP请求者,而OCSP服务器通常称为OCSP响应器。OCSP协议工作方式如下,
- ① OCSP客户端应用软件产生OCSP请求OCSPRequest,发送给OCSP响应器。请求里面包含查询证书的证书标识。
- ② OCSP响应器收到请求后,首先检查请求的编码格式,若格式不对,则直接返回给客户端错误信息(不用签名);否则,响应器从请求中取出需要查询证书的证书标识。
- ③ 响应器取得待验证证书的证书标识后,若不能识别该证书的颁发机构(即响应器数据源中没有该证书颁发机构的证书撤销信息),则返回给客户端OCSP响应OCSPResponse,其中的证书状态为“未知”;否则,响应器查询响应器数据源,根据查找结果返回证书状态为“正常”或“撤销”的响应。这些响应都需要签名。
- ④ OCSP客户端应用软件收到带签名的OCSP响应后,要做以下验证。如果所有验证都通过,那么客户端就接受这个响应;否则就认为这个响应无效。
(有关协议更多详情可参考:http://www.cnpaf.net/Class/Rfcen/200502/3610.html)
3.1、OCSP请求消息描述
OCSP请求包括的信息主要有:协议版本号、请求者标识、目标证书标识和可选的扩展项等。
- 一个OCSP请求由请求消息和消息签名组成,消息签名是可选的。在OCSP请求中加入签名是为了使OCSP响应器能认证请求者的身份。若请求方选择签名OCSP请求,则对整个的tbsRequest域计算签名。
- TBSRequest域的内容为实际的请求,它由版本号、请求者名称(可选)、请求列表、可选的扩展项四部分组成。请求中最重要的域就是请求证书列表,请求证书列表是一个结构序列,可以包含一个或多个证书请求。
- Request域是有目标证书标识和可选的单一请求扩展组成。
- CertID(证书标识)中包含了Hash算法标识、证书颁发者(CA)名称(DN)的Hash值、证书颁发者(CA)公钥的Hash值和证书序列号。
- Signature域是有签名算法标识、签名值和帮助响应器验证请求者签名的证书组成,其中签名值是对整个的tsbRequest域计算签名。
当OCSP响应器收到请求之后,它会检查请求的内容:
- ① 请求信息格式是否正确;
- ② 该响应器是否被配置为提供应答服务;
- ③ 请求中是否包含了响应器需要的信息。
若检查通过,返回一个确定的回复,OCSP响应端回复的加密消息中包含证书的状态可以是:
- good 表示该证书没有被撤销,但是这并不意味着该证书曾被发型或响应产生时是在证书的有效时间间隔内;
- revoked 表示证书被撤销;
- unknown 表示响应器不能判断请求的证书状态。
若任何一个先决条件没有满足,那么OCSP响应器将产生一个错误信息,错误码可能包含以下内容:
- malformedRequest:未正确格式化的请求
- internalError:内部错误
- trylater:请稍后再试
- sigRequired:需要签名
- unauthorized:未授权
3.2、OCSP响应消息描述
OCSP响应可能是一个确定的响应或一个标识异常情况的出错信息。对每个确定的响应,响应器必须签名,对出错信息不必签名。确定的响应中包含协议版本号、响应器名、对每一张待查询证书的回复、可选的扩展项、签名算法对象标识和签名值。
- OCSP响应是由响应状态和响应字节组成。只有当responseStatus的值为“successful”时,responseBytes的值才会被设置(即如果响应状态为某一种出错情况,那么响应字节将不被设置)。
- responseStatus响应状态有六种取值:“successful”表示本响应时有效的响应;“malformedRequest”表明接收的请求同OCSP的语法不一致;“internalError”指出OCSP响应器处于一个不协调的内部状态,请求需要再试,暗示尝试另一个响应器;“trylater”用于指出该服务仍然存在,但是暂时不能够响应;“sigRequired”的响应当服务器需要客户方签名时返回;“unauthorized”的响应当客户方没有权限查询该响应器时返回。
- 响应字节的值由响应类型标识和一个编码成OCTET字符串的响应信息(此信息由类型标识确定)组成。
- 响应信息的值应该是基本OCSP响应 (BasicOCSPResponse)的DER编码。基本OCSP响应是由证书状态响应数据、签名算法标识、签名值和帮助请求者验证签名的证书组成,其中签名值是对响应数据的签名结果。
- 响应数据包括版本号、响应器标识、响应产生时间、响应列表和可选的扩展项。
- 如果一个OCSP请求中包含多个证书的查询请求,那么响应列表(responses)就列出了请求中所有证书状态的响应。每个证书状态响应(SingleResponse)包含证书标识、证书状态、本次更新时间、下次更新时间(可选)和扩展项。
- 证书标识和请求报文中certID是一致的,它唯一标识了一个证书。
所有确定的响应消息必须经过数字签名以保证响应是源于可信任方并且传输过程中没有被改动。用来签名响应消息的密钥必须是下列中的一个:
- 颁发目标证书的CA;
- 一个可信的响应器,它的公钥被请求者信任;
- 经颁发证书的CA认可(通过授权)的实体。
当接收一个签名响应时,OCSP客户方必须检测一下几项:
- 接收的响应中标识的证书与请求是否一致;
- 响应的签名是否有效;
- 响应签名者是否被客户端信任(响应器证书是否在客户端的信任列表中);
- 响应中的响应产生时间是否是当前时间。
只有当上述所有条件都得到确认之后,客户方才可接收有响应器传送过来的证书状态信息。
4、OpenSSL编码实现
openssl在crypto/ocsp目录实现了ocsp模块,包括客户端和服务端各种函数:
- ocsp_asn.c :ocsp消息的DER编解码实现,包括基本的new\free\i2d和d2i函数
- ocsp_cl.c:ocsp客户端函数实现,主要用于生成ocsp请求
- ocsp_srv.c:ocsp服务端思想,主要用于生成ocsp响应
- ocsp_err.c:oscp错误处理
- ocsp_ext.c:ocsp扩展项处理
- ocsp_ht.c:基于HTTP协议通信的OCSP实现
- ocsp_lib.c:通用库实现
- ocsp_prn :打印OCSP信息
- ocsp_vfy: 验证ocsp请求和响应
- ocsp.h:定义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 //未授权的
- 如果responder收到的请求不符合OCSP语义,就会回复(malformedRequest);
- internalError表示responder出现了处理错误,请求应该再次被发起,最好发送到其他的responder;
- 如果responder是开启的但是不能响应证书的状态,就会回复“trytLater”;
- unauthorized表示客户端未被授权向这台responder发起请求。
9、接下来,我们需要知道回复的详细信息,以确定回复是否符合我们的要求。
主要从以下几项进行确认:
- a. 检查nonce。同样,并非所有的 CA 都支持随机数。(根据实际情况,该项可不检查)
- b. 在回复信息中所指的证书和相应请求中所指证书一致。(即响应中的证书id必须和请求的证书id一致)
- c. 检查响应是否有有效签名。没有有效的签名,我们不能相信它。
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
- good 状态表示一个对状态查询的积极回复。至少,这个积极回复表示这张证书没有被撤消,但是不一定意味着这张证书曾经被颁发过或者产生这个回复在证书有效期内。回复扩展可以被用来传输一些附加信息,响应器由此可以对这张证书的状态做出一些积极的声明,诸如(已颁发)保证,有效期等等。
- revoked 状态表示证书已被撤消(无论是临时性的还是永久性的(待判断))
- unknow 状态表示响应器不知道请求的证书。
11、最后检查有效性。时间检查。
int nsec = 5 * 60, maxage = -1;
......
......
//时间检查计算,合法返回1,thisupd为本次更新时间,nextupd为下次更新时间
if (1 != OCSP_check_validity(thisupd, nextupd, nsec, maxage))
{
//ocsp检查不合法。
//TODO:错误处理......
}
哦了,港完收工(((((((((((っ•ω•)っ Σ(σ`•ω•´)σ 起飞!