一、服务端和客户端校验自己证书
服务端和客户端在tls_construct_server_certificate/tls_construct_client_certificate阶段都会进行对自己的证书进行校验,在tls_construct_server_certificate/tls_construct_client_certificate都调用了函数ssl3_output_cert_chain
unsigned long ssl3_output_cert_chain(SSL *s, CERT_PKEY *cpk) {
......
if (!ssl_add_cert_chain(s, cpk, &l))
return 0;
......
}
然后进入ssl_add_cert_chain函数,部分函数如下
/* Add certificate chain to internal SSL BUF_MEM structure */
int ssl_add_cert_chain(SSL *s, CERT_PKEY *cpk, unsigned long *l)
{
......
if (chain_store) {
X509_STORE_CTX *xs_ctx = X509_STORE_CTX_new();
if (xs_ctx == NULL) {
SSLerr(SSL_F_SSL_ADD_CERT_CHAIN, ERR_R_MALLOC_FAILURE);
return (0);
}
if (!X509_STORE_CTX_init(xs_ctx, chain_store, x, NULL)) {
X509_STORE_CTX_free(xs_ctx);
SSLerr(SSL_F_SSL_ADD_CERT_CHAIN, ERR_R_X509_LIB);
return (0);
}
(void)X509_verify_cert(xs_ctx);
/* Don't leave errors in the queue */
ERR_clear_error();
......
}
return 1;
}
其中X509_STORE_CTX这个结构主要是用来校验证书用,一般都会使用X509_STORE这个已经设置好的对象来初始化X509_STORE_CTX,初始化函数如下:
int X509_STORE_CTX_init(X509_STORE_CTX *ctx, X509_STORE *store,
X509 *x509, STACK_OF(X509) *chain){
......
if (store && store->verify)
ctx->verify = store->verify;
else
ctx->verify = internal_verify;
......
}
这个初始化函数参数说明一下,第二个参数store是用来初始化第一个参数ctx,第三个参数x509表示待验证的证书,chain是不信任证书链。
具体的校验函数也是在该init函数里面设定的,如果自己不需要(一般也不需要),那么使用gmssl自带实现的internal_verify函数来进行证书链的校验。
上面忽略了验证证书的函数X509_verify_cert的返回值处理,其解释如下
链不完整是有效的(因为通常我们不在链中包括根证书)。因此,我们故意忽略此调用返回的错误。我们实际上并不是在验证证书——我们只是在尽可能多地构建链
进入X509_verify_cert函数对证书链的内容进行校验,
int X509_verify_cert(X509_STORE_CTX *ctx)
{
......
if (DANETLS_ENABLED(dane))
ret = dane_verify(ctx);
else
ret = verify_chain(ctx);
......
return ret;
}
首先需要构造一个待校验的证书链,该证书链的末端为待校验的证书,证书链的顶端为自签名ca证书,并且顶端ca证书必须是本地store中保存已有的。构造好了这么一个证书链之后,再一级一级的从顶端证书校验到末尾,即完成了整个证书链的校验。
接着进入函数verify_chain(ctx)
static int verify_chain(X509_STORE_CTX *ctx)
{
int err;
int ok;
/*
* Before either returning with an error, or continuing with CRL checks,
* instantiate chain public key parameters.
*/
if ((ok = build_chain(ctx)) == 0 ||
(ok = check_chain_extensions(ctx)) == 0 ||
(ok = check_auth_level(ctx)) == 0 ||
(ok = check_name_constraints(ctx)) == 0 ||
(ok = check_id(ctx)) == 0 || 1)
X509_get_pubkey_parameters(NULL, ctx->chain);
if (ok == 0 || (ok = ctx->check_revocation(ctx)) == 0)
return ok;
......
/* Verify chain signatures and expiration times */
ok = (ctx->verify != NULL) ? ctx->verify(ctx) : internal_verify(ctx);
if (!ok)
return ok;
......
}
在verify_chain函数中ctx->verify(ctx)在上面函数ssl_add_cert_chain中通过X509_STORE_CTX_init(xs_ctx, chain_store, x, NULL)进行初始化。另外这个init函数中还设置了很多回调函数,基本上都是把X509_STORE的函数抄一份给X509_STORE_CTX。
if (store && store->verify)
ctx->verify = store->verify;
else
ctx->verify = internal_verify;
下面开始具体讲一讲internal_verify这个函数。进入internal_verify函数中验证链签名和过期时间,internal_verify函数如下
static int internal_verify(X509_STORE_CTX *ctx)
{
......
while (n >= 0) {
EVP_PKEY *pkey;
/*
* Skip signature check for self signed certificates unless explicitly
* asked for. It doesn't add any security and just wastes time. If
* the issuer's public key is unusable, report the issuer certificate
* and its depth (rather than the depth of the subject).
*/
if (xs != xi || (ctx->param->flags & X509_V_FLAG_CHECK_SS_SIGNATURE))
{
if ((pkey = X509_get0_pubkey(xi)) == NULL) {
if (!verify_cb_cert(ctx, xi, xi != xs ? n+1 : n,
X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY))
return 0;
} else if (X509_verify(xs, pkey) <= 0) {
if (!verify_cb_cert(ctx, xs, n,
X509_V_ERR_CERT_SIGNATURE_FAILURE))
return 0;
}
}
check_cert:
/* Calls verify callback as needed */
if (!x509_check_cert_time(ctx, xs, n))
return 0;
/*
* Signal success at this depth. However, the previous error (if any)
* is retained.
*/
ctx->current_issuer = xi;
ctx->current_cert = xs;
ctx->error_depth = n;
if (!ctx->verify_cb(1, ctx))
return 0;
if (--n >= 0) {
xi = xs;
xs = sk_X509_value(ctx->chain, n);
}
}
return 1;
}
判断证书链里面的证书个数,对证书链中的证书,验证深度为证书个数减1,首先通过函数x509_check_cert_time验证证书链中每个证书的时间有效期是否过期,接着对证书进行验签,通过CA证书中的公钥进行验签。
除非明确要求,否则跳过自签名证书的签名检查。它没有增加任何安全性,只是浪费时间。如果颁发者的公钥不可用,请报告颁发者证书及其深度(而不是主题的深度)。
verify的参数设定了证书验证的深度,超过此深度,即视为证书验证失败。所谓证书验证深度,就是证书链(这里指服务器证书的证书链)的长度,即从根CA到服务器证书的层数,如果签发服务器证书的CA本身就是根CA,那么证书链长度就为1
对证书是否过期进行判断,通过函数x509_check_cert_time
int x509_check_cert_time(X509_STORE_CTX *ctx, X509 *x, int depth)
{
time_t *ptime;
int i;
//ctx->param->flags=0x8000
if (ctx->param->flags & X509_V_FLAG_USE_CHECK_TIME)
ptime = &ctx->param->check_time;
else if (ctx->param->flags & X509_V_FLAG_NO_CHECK_TIME)
return 1;
else
ptime = NULL;
i = X509_cmp_time(X509_get0_notBefore(x), ptime);
if (i >= 0 && depth < 0)
return 0;
if (i == 0 && !verify_cb_cert(ctx, x, depth,
X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD))
return 0;
if (i > 0 && !verify_cb_cert(ctx, x, depth,
X509_V_ERR_CERT_NOT_YET_VALID))
return 0;
i = X509_cmp_time(X509_get0_notAfter(x), ptime);
if (i <= 0 && depth < 0)
return 0;
if (i == 0 && !verify_cb_cert(ctx, x, depth,
509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD))
return 0;
if (i < 0 && !verify_cb_cert(ctx, x, depth, x509_V_ERR_CERT_HAS_EXPIRED))
return 0;
return 1;
}
该函数通过X509_cmp_time的返回值和verify_cb_cert函数来判断是否通过
其中X509_cmp_time函数如下:
int X509_cmp_time(const ASN1_TIME *ctm, time_t *cmp_time)
{
char *str;
ASN1_TIME atm;
long offset;
char buff1[24], buff2[24], *p;
int i, j, remaining;
p = buff1;
remaining = ctm->length;
str = (char *)ctm->data;
/*
* Note that the following (historical) code allows much more slack in the
* time format than RFC5280. In RFC5280, the representation is fixed:
* UTCTime: YYMMDDHHMMSSZ
* GeneralizedTime: YYYYMMDDHHMMSSZ
*/
if (ctm->type == V_ASN1_UTCTIME) {
/* YYMMDDHHMM[SS]Z or YYMMDDHHMM[SS](+-)hhmm */
int min_length = sizeof("YYMMDDHHMMZ") - 1;
int max_length = sizeof("YYMMDDHHMMSS+hhmm") - 1;
if (remaining < min_length || remaining > max_length)
return 0;
memcpy(p, str, 10);
p += 10;
str += 10;
remaining -= 10;
} else {
/* YYYYMMDDHHMM[SS[.fff]]Z or YYYYMMDDHHMM[SS[.f[f[f]]]](+-)hhmm */
int min_length = sizeof("YYYYMMDDHHMMZ") - 1;
int max_length = sizeof("YYYYMMDDHHMMSS.fff+hhmm") - 1;
if (remaining < min_length || remaining > max_length)
return 0;
memcpy(p, str, 12);
p += 12;
str += 12;
remaining -= 12;
}
if ((*str == 'Z') || (*str == '-') || (*str == '+')) {
*(p++) = '0';
*(p++) = '0';
} else {
/* SS (seconds) */
if (remaining < 2)
return 0;
*(p++) = *(str++);
*(p++) = *(str++);
remaining -= 2;
/*
* Skip any (up to three) fractional seconds...
* TODO(emilia): in RFC5280, fractional seconds are forbidden.
* Can we just kill them altogether?
*/
if (remaining && *str == '.') {
str++;
remaining--;
for (i = 0; i < 3 && remaining; i++, str++, remaining--) {
if (*str < '0' || *str > '9')
break;
}
}
}
*(p++) = 'Z';
*(p++) = '\0';
/* We now need either a terminating 'Z' or an offset. */
if (!remaining)
return 0;
if (*str == 'Z') {
if (remaining != 1)
return 0;
offset = 0;
} else {
/* (+-)HHMM */
if ((*str != '+') && (*str != '-'))
return 0;
/* Historical behaviour: the (+-)hhmm offset is forbidden in RFC5280. */
if (remaining != 5)
return 0;
if (str[1] < '0' || str[1] > '9' || str[2] < '0' || str[2] > '9' ||
str[3] < '0' || str[3] > '9' || str[4] < '0' || str[4] > '9')
return 0;
offset = ((str[1] - '0') * 10 + (str[2] - '0')) * 60;
offset += (str[3] - '0') * 10 + (str[4] - '0');
if (*str == '-')
offset = -offset;
}
atm.type = ctm->type;
atm.flags = 0;
atm.length = sizeof(buff2);
atm.data = (unsigned char *)buff2; //buff2中储存当前时间
//获取当前设备时间
if (X509_time_adj(&atm, offset * 60, cmp_time) == NULL)
return 0;
if (ctm->type == V_ASN1_UTCTIME) {
i = (buff1[0] - '0') * 10 + (buff1[1] - '0');
if (i < 50)
i += 100; /* cf. RFC 2459 */
j = (buff2[0] - '0') * 10 + (buff2[1] - '0');
if (j < 50)
j += 100;
if (i < j)
return -1;
if (i > j)
return 1;
}
i = strcmp(buff1, buff2);
if (i == 0) /* wait a second then return younger :-) */
return -1;
else
return i;
}
判断证书时间有效期是否过期逻辑如下:
-
判断现有时间长度是否满足条件,使用UTCtime格式为YYMMDDHHMMSSZ
-
取出notbefore的时间中的年份,上面buff1和buff2中取出的为ASICC格式,算出的i判断与50的大小
当YY大于等于50,年将被认为是19YY;
当YY不到50,年将被认为是20YY。
参考:<RFC2459 Internet X.509 公钥基础设施>
-
得到时间后判断notbefore时间是否满足要求,当满足要求,i<j,return -1
-
同理notafter时间是否满足要求,当满足要求,i>j,return 1
-
若当前设备时间与notbefore或notafter年份一致,则进行直接比较buff1和buff2的内容,buff1为notbefore/notafter的时间,buff2为当前设备时间,输出格式如下
证书时间为:
notBefore=May 29 08:48:26 2019 GMT
notAfter=Jul 7 08:48:26 2023 GMT
buff1=190529084826Z
buff2=220705004647Z
buff1=230707084826Z
buff2=220705004647Z
在x509_check_cert_time函数中,当时间不满足要求的时候,返回值需要根据verify_cb_cert来确定。
其中verify_cb_cert函数返回值为
return ctx->verify_cb(0, ctx);
上述的返回值在函数ssl_verify_cert_chain->X509_STORE_CTX_set_verify_cb进行初始化,
X509_STORE_CTX_set_verify_cb(ctx, s->verify_callback);
而s->verify_callback所指向的函数为自己写的callabck函数
可以提供参考如下:
static int verify_callback(int ok, X509_STORE_CTX *ctx){
int cert_error = X509_STORE_CTX_get_error(ctx);
if (!ok) {
switch (cert_error) {
case X509_V_ERR_UNABLE_TO_GET_CRL_ISSUER: //33
case X509_V_ERR_KEYUSAGE_NO_CRL_SIGN: //35
case X509_V_ERR_DIFFERENT_CRL_SCOPE: //44
case X509_V_ERR_CRL_PATH_VALIDATION_ERROR: //54
case X509_V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD: //15
case X509_V_ERR_CRL_NOT_YET_VALID: //11
case X509_V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD: //16
case X509_V_ERR_CRL_HAS_EXPIRED: //12
case X509_V_ERR_CRL_SIGNATURE_FAILURE: //8
case X509_V_ERR_UNABLE_TO_GET_CRL: //3
ok = 1;
/*要求:证书撤销列表本地的错误,不应该导致客户端连接不成功*/
/*检查证书的时候,若失败,线程投递错误,导致ssl_read失败*/
/*在回调中修改了函数返回值,让crl的错误返回成功,但是错误还投递了,这里让他清空一下。*/
ERR_clear_error();
break;
}
}
return (ok);
}
根据上面代码可知,即使在函数x509_check_cert_time中判断时间的返回值i满足条件,但是上面verify_callbacka函数中ok=0,cert_error为X509_V_ERR_CERT_HAS_EXPIRED,为10,解释为证书过期,但是不在上面的case中,所以返回ok=0,导致证书有效性验证失败
二、服务端和客户端校验对端证书
服务端和客户端在tls_process_server_certificate/tls_process_client_certificate阶段都会进行对对端的证书进行校验,在tls_proces_server_certificate/tls_proces_client_certificate都调用了函数ssl_verify_cert_chain,在ssl_verify_cert_chain中调用了X509_verify_cert,后面的验证内容与上述一致,但是服务端和客户端的返回值处理不一致
客户端
MSG_PROCESS_RETURN tls_process_server_certificate(SSL *s, PACKET *pkt){
......
i = ssl_verify_cert_chain(s, sk);
if (s->verify_mode != SSL_VERIFY_NONE && i <= 0) {
al = ssl_verify_alarm_type(s->verify_result);
SSLerr(SSL_F_TLS_PROCESS_SERVER_CERTIFICATE,
SSL_R_CERTIFICATE_VERIFY_FAILED);
goto f_err;
}
......
}
根据上面代码,在tls_process_server_certificate中,当验证证书有效期失败后,并且当s->verify_mode不为SSL_VERIFY_NONE(默认为0)的时候才会验证失败,从而连接失败,然而在自己的代码中中设置了s->verify_mode,如
SSL_CTX_set_verify(ctx_62351.g_sslCtx, SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT, verify_callback);
则证书过期才会导致;连接失败,若不设置,则客户端不进行断连操作
服务端
MSG_PROCESS_RETURN tls_process_client_certificate(SSL *s, PACKET *pkt)
{
......
i = ssl_verify_cert_chain(s, sk);
if (i <= 0) {
al = ssl_verify_alarm_type(s->verify_result);
SSLerr(SSL_F_TLS_PROCESS_CLIENT_CERTIFICATE,
SSL_R_CERTIFICATE_VERIFY_FAILED);
ret = SSL_R_CERTIFICATE_VERIFY_FAILED;
goto f_err;
}
if (i > 1) {
SSLerr(SSL_F_TLS_PROCESS_CLIENT_CERTIFICATE, i);
al = SSL_AD_HANDSHAKE_FAILURE;
goto f_err;
}
......
}
若对端证书验证错误,i != 1,将会直接报错,TLS连接断开