OpenSSLでのOCSP Stapling
以前、「OpenSSLでのサーバー証明書の検証」の記事でOpenSSLを使ったTLS接続時に証明書の検証を行う方法を紹介しましたが、今回は接続時にOCSP(Online Certificate Status Protocol) Staplingによって証明書の失効チェックを行う例を示します。
「OpenSSLでのサーバー証明書の検証」の証明書検証のソースをベースに必要な処理を追加する形で説明します。使用するOpenSSLのバージョンは1.1.1です。
実装
まず、TLS接続時にstatus_request 拡張を設定しておき、クライアントがOCSP Staplingに対応していることを示す必要があります。
status_request TLS拡張を設定
/* status_request TLS拡張 */ SSL_set_tlsext_status_type(ssl, TLSEXT_STATUSTYPE_ocsp); /* OCSPレスポンスの受信コールバックを指定 */ SSL_CTX_set_tlsext_status_cb(ctx, status_cb);
TLS拡張の設定はTLS接続(SSL_connect())前に設定しておく必要があります。「OpenSSLでのサーバー証明書の検証」のソースのSSL_set_tlsext_host_name(ssl, hostname);の呼び出し後あたりにでも追加しておけばよいでしょう。
TLS拡張の設定に合わせて、OCSPレスポンスの受信コールバックを設定しておきます。
OCSPレスポンス受信コールバックの実装
次にSSL_CTX_set_tlsext_status_cb()で登録したOCSPレスポンスの受信コールバックを作成します。
このコールバックはstatus_request 拡張を設定していた場合は、TLS接続後に必ず呼び出されます。OCSPレスポンスが受信できた場合はもちろん呼び出されますが、サーバーがOCSP Staplingに対応しておらず、OCSPレスポンスが返されなかった場合でも呼び出されます。
OCSPレスポンスの受信コールバック
int status_cb(SSL *ssl, void *arg) { unsigned char *resp; long resp_len; const unsigned char *in; OCSP_RESPONSE *ocsp_resp = NULL; X509 *cert; STACK_OF(X509) *chain; int ret = -1; /* error */ int result; resp_len = SSL_get_tlsext_status_ocsp_resp(ssl, &resp); if (resp == NULL) { fprintf(stderr, "No OCSP response\n"); return 1; /* soft fail */ } /* OCSP Response取得 */ in = resp; ocsp_resp = d2i_OCSP_RESPONSE(NULL, &in, resp_len); if(!ocsp_resp) { fprintf(stderr, "Can't read OCSP response.\n"); goto error; } cert = SSL_get_peer_certificate(ssl); if(!cert) { fprintf(stderr, "Can't get peer certificate.\n"); goto error; } chain = SSL_get_peer_cert_chain(ssl); if (!chain) { fprintf(stderr, "Can't get peer certificate chain.\n"); goto error; } result = check_ocsp_response(ocsp_resp, cert, chain, SSL_CTX_get_cert_store(SSL_get_SSL_CTX(ssl))); if (result < 1) { ret = result; /* invalid status or error */ goto error; } OCSP_RESPONSE_free(ocsp_resp); return 1; error: if (ocsp_resp) { OCSP_RESPONSE_free(ocsp_resp); } return ret; /* 0 or -1 */ }
このコールバックでは、まず、SSL_get_tlsext_status_ocsp_resp()でOCSPレスポンスの長さを取得します。レスポンス長が0の場合は、OCSPレスポンスが返されていないことを示しています(理由は恐らく、サーバーがOCSP Staplingに未対応のため)。
OCSPレスポンスがない場合は、失効の判定はできないのですが、この場合は一般的に成功扱いにします(そうでないとOCSP Staplingに未対応のサーバーに接続できなくなってしまうので)。
OCSPレスポンスが存在した場合は、check_ocsp_response()でOCSPレスポンス自体のチェックと証明書の失効状態の取得を行います。証明書の失効チェック(check_ocsp_response())には確認対象の証明書(cert)の他に中間証明書のリスト(chain)とCA証明書(store)が必要になります。
OCSPレスポンスチェック処理の実装
check_ocsp_response()でOCSPレスポンスのチェックを行っていきますが、まずOCSPレスポンスのだいたいの構造を図1に示します(*1)。

まず、レスポンスの結果を示すOCSP Response Statusがあり、'successful'ならBasic OCSP Responseが続きます。
Basic Responseには失効確認をRequestした証明書のCertificate ID(*2)と確認結果であるCert Status(Good/Revoked/Unknown)が含まれます。この証明書情報は複数リクエストした場合は、その分だけ返されますが、OCSP Staplingではリクエストは一つなのでレスポンスも一つになります。また、レスポンスの偽造を防ぐための電子署名があります。
OCSPレスポンスのチェック処理
/* lookup an issuer cert of cert from sk_x509. */ X509 *lookup_issuer_cert(STACK_OF(X509) *sk_x509, X509 *cert) { int i; for(i = 0 ; i < sk_X509_num(sk_x509) ; i++) { X509 *issuer = sk_X509_value(sk_x509, i); if(X509_check_issued(issuer, cert) == X509_V_OK) { return issuer; } } /* not found */ return NULL; } /* * Check the OCSP response to make sure that the certificate has not revoked. * Return: * 1: Success (Certificate Status is "Good") * 0: Invalid certificate status (revoked certificate and so on) * -1: Error */ int check_ocsp_response(OCSP_RESPONSE *ocsp_resp, X509* cert, STACK_OF(X509) *chain, X509_STORE *store) { int resp_status; OCSP_BASICRESP *basic_resp = NULL; X509 *issuer; OCSP_CERTID *cert_id = NULL; int status, reason; ASN1_GENERALIZEDTIME *rev, *this_update, *next_update; int ret = -1; /* error */ resp_status = OCSP_response_status(ocsp_resp); fprintf(stderr, "OCSP Response Status: %s (%d)\n", OCSP_response_status_str(resp_status), resp_status); if(resp_status != OCSP_RESPONSE_STATUS_SUCCESSFUL) { goto error; } /* Get Basic OCSP Response */ basic_resp = OCSP_response_get1_basic(ocsp_resp); if(!basic_resp) { fprintf(stderr, "Can't get Basic OCSP Response.\n"); goto error; } /* * Check the validity of Basic Response. * - Signature Validity * - Signer Certificate Validity */ if(OCSP_basic_verify(basic_resp, chain, store, 0) <= 0) { fprintf(stderr, "OCSP response verification failed.\n"); goto error; } /* Calculate certificate ID */ issuer = lookup_issuer_cert(chain, cert); if (issuer == NULL) { fprintf(stderr, "Issuer certificate not found.\n"); goto error; } cert_id = OCSP_cert_to_id(EVP_sha1(), cert, issuer); if(!cert_id) { fprintf(stderr, "Can't create Certificate ID.\n"); goto error; } if (OCSP_resp_find_status(basic_resp, cert_id, &status, &reason, &rev, &this_update, &next_update) == 0) { fprintf(stderr, "Certificate ID not found.\n"); goto error; } fprintf(stderr, "Certificate Status: %s (%d)\n", OCSP_cert_status_str(status), status); if (status != V_OCSP_CERTSTATUS_GOOD) { if (status == V_OCSP_CERTSTATUS_REVOKED) { fprintf(stderr, "Revocation Reason: %s (%d)\n", OCSP_crl_reason_str(reason), reason); } ret = 0; goto error; } /* Checking this_update, next_update */ if(!OCSP_check_validity(this_update, next_update, 300, -1)) { fprintf(stderr, "Invalid This Update or Next Update.\n"); goto error; } OCSP_BASICRESP_free(basic_resp); OCSP_CERTID_free(cert_id); return 1; error: if (basic_resp) { OCSP_BASICRESP_free(basic_resp); } if (cert_id) { OCSP_CERTID_free(cert_id); } return ret; /* 0 or -1 */ }
check_ocsp_response()の説明に戻ると、まず、OCSP_response_status()でレスポンス自体のstatusチェックを行います。statusがOCSP_RESPONSE_STATUS_SUCCESSFULならResponse内にBasic OCSP Responseが存在するのでOCSP_response_get1_basic()でBasicレスポンスを取得します。
その後、OCSP_basic_verify()で取得したBasicレスポンスの妥当性をチェックします。具体的には電子署名のチェックを行っています。
OCSPではOCSPレスポンダとの通信はhttp(非TLS)なので、署名をチェックして本当にIssuerが作成したレスポンスなのかを確認する必要があります。さらにOCSP StaplingにおいてはWebサーバーがOCSPリクエストを代行することになるため、Webサーバーが虚偽のレスポンスを返していないことを確認することにもなります。
先程、証明書の失効チェックには「確認対象の証明書(cert)の他に中間証明書のリスト(chain)とCA証明書(store)が必要になります。」と書きましたが、これは中間証明書やCA証明書がこの電子署名の確認で必要になるためです(*3)。
これで、レスポンスのデータに問題ないことは確認できたので、失効状態を確認するだけです。
確認対象の証明書のCertificate IDを計算し、OCSP_resp_find_status()でレスポンス内から指定Certificate IDの証明書情報を取得します。Certificate StatusがV_OCSP_CERTSTATUS_GOODなら正常(失効していない)です。
最後にCertificate Statusの有効期限のチェックをOCSP_check_validity()で行い完了です。
[関連記事]
(*1) この出力はopensslコマンドで以下のようにOCSPを実行すれば得られます。
openssl ocsp -issuer <isser cert file> -cert <cert file> -text -url <the ocsp responder URL>
(*2) OCSPでは証明書全体のデータをやり取りするのではなく、(1) シリアルNo. (2) Issuer名のハッシュ値 (3) Issuerの公開鍵のハッシュ値 の組み合わせをCertificate IDとしてIDで証明書を識別するようになっています。
(*3) opssl ocsp コマンドで -issuer 引数が必要なのも同じ理由です。
投稿日:2021/12/27 23:51