x509证书有效期校验过程_基于TLS1.2(GmSSL)

1 篇文章 0 订阅

一、服务端和客户端校验自己证书

服务端和客户端在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连接断开

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值