✔️

OCSP verifier を Go で実装する

2023/08/14に公開

OCSP (Online Certificate Status Protocol) は RFC 6960 で規定されている証明書の状態確認のためのプロトコルです。

Go では OCSP リクエスト生成、レスポンス解析のための package は用意されていますが、OCSP verifier の公式実装はありません。

次の proposal が提出されています。

https://github.com/golang/go/issues/40017

こちらの proposal と RFC 6960 を読みながら、勉強のために簡易的な OCSP verifier を書いてみました。

前提

  • Go 1.21
  • 今回は、Apple PKI で配布されている Worldwide Developer Relations - G6 証明書の失効確認を例にします。
    • 当該証明書は同じく Apple PKI で配布されている Apple Root CA - G3 Root により署名されており、今回このルート証明書は有効かつ失効しておらず、信用しているという前提とします。
  • Delegated OCSP Responder の証明書の失効確認は行わず、id-pkix-ocsp-nocheck extension が存在しない場合は検証失敗とします。
  • リクエストが失敗した際の自動的な再試行 (exponential backoff 等) は実装していません。

実装

説明不要で、コード全体が見たい方は以下からどうぞ。

https://gist.github.com/ww24/3e2af7f82aaf1b986a130bce78bfdebd

OCSP リクエストの作成

ocspRequest, err := ocsp.CreateRequest(cert, issuer, &ocsp.RequestOptions{Hash: crypto.SHA256})
if err != nil {
	return err
}

ocsp.CreateRequest を使用します。

  • 第1引数の cert は状態を確認したい証明書 (*x509.Certificate)
  • 第2引数の issuercert に署名している証明書 (*x509.Certificate)
  • 第3引数には *ocsp.RequestOptions を指定します。
    • Hash アルゴリズムのみ指定することが出来ます。今回は crypto.SHA256 を指定します。
    • nil or 空 (&ocsp.RequestOptions{}) を指定すると crypto.SHA1 がデフォルトで使用されます。
req, err := http.NewRequest(http.MethodPost, ocspServer, bytes.NewReader(ocspRequest))
if err != nil {
	return err
}
req.Header.Set("Content-Type", "application/ocsp-request")
req.Header.Set("Accept", "application/ocsp-response")

ocspServer は OCSP Responder の URL です。
証明書の AIA (Authority Information Access) Extension に含まれている場合、cert.OCSPServer で参照できます。

HTTP リクエストを作成し、リクエストヘッダーを指定します。
OCSP リクエストとレスポンスの MIME Type は Appendix A. OCSP over HTTP に記載があります。

Header Value
Content-Type application/ocsp-request
Accept application/ocsp-response

OCSP リクエストを Base64 encode したサイズが 255 bytes 未満であれば GET リクエストしても良いとされているため、GET method でリクエストする場合は次のようになります。

const ocspGetRequestThreshold = 255
base64EncodedRequest := base64.StdEncoding.EncodeToString(ocspRequest)
if len(base64EncodedRequest) < ocspGetRequestThreshold {
	var err error
	base64EncodedRequest := base64.StdEncoding.EncodeToString(ocspRequest)
	req, err = http.NewRequest(http.MethodGet, ocspServer+"/"+base64EncodedRequest, http.NoBody)
	if err != nil {
		return err
	}
	req.Header.Set("Accept", "application/ocsp-response")
}

主にキャッシュの観点からの利用を想定しているようです。
今回 Apple の OCSP Responder は POST でもキャッシュが効いている (Age Header が返る) ようなので、パフォーマンス上大きな差異はなくコードをシンプルに保つという観点から GET は実装しないことにしました。

リクエストの発行と、レスポンスの解析

httpClient := &http.Client{Timeout: 5 * time.Second}
resp, err := httpClient.Do(req.WithContext(ctx))
if err != nil {
	return err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
	return err
}

ocspResponse, err := ocsp.ParseResponseForCert(body, cert, issuer)
if err != nil {
	var responseErr ocsp.ResponseError
	if errors.As(err, &responseErr) {
		return fmt.Errorf("ocsp status: %s", responseErr.Status.String())
	}
	return err
}

HTTP リクエストを実行し、レスポンスを ocsp.ParseResponseForCert に渡します。

第2、3引数の certissuerocsp.CreateRequest に渡したものと同じです。
issuer は省略することが出来ますが、その場合署名の検証が省略されるので、別途 OCSP Response の署名検証を行わない限り指定した方が良いでしょう。

戻り値として *ocsp.Response もしくは error が返ります。
2つ目の戻り値の errorocsp.ResponseError の場合、ocsp.ResponseStatus が取得できます。

ocsp.ResponseStatus の各状態の詳細については RFC 6960 Section 2.3. Exception Cases を参照してください。

OCSP レスポンスの検証

OCSP Responder が返却したレスポンスが正規のものであるか検証する必要があります。
ocspResponse.Certificate が空の場合かつ、ocsp.ParseResponseForCertissuer を渡している場合は、issuer によって OCSP Response が署名されている事が検証されています。

ocspResponse.Certificate に証明書が設定されている場合、OCSP Response がその証明書で署名されている事と、証明書が issuer によって署名されていることは検証されていますが、それだけでは不十分です。

OCSP クライアントでの OCSP レスポンスの受け入れ要件が RFC 6960 Section 3.2. Signed Response Acceptance Requirements に書かれています。

加えて、RFC 6960 Section 4.2.2.2. Authorized Responders に OCSP Responder の認証についての記載があります。

Delegated OCSP Responder の場合、CA から正しく委任を受けた OCSP Responder であることを認証する必要があります。

if ocspResponse.Certificate.IsCA {
	return errors.New("ocsp response certificate should be end-entity certificate")
}

これは RFC 6960 に記載されていませんが、proposal に記載のあるエンド・エンティティ証明書 (リーフ証明書) であることの確認です。[1]

if ocspResponse.Certificate.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
	return errors.New("ocsp response certificate has no digital signature key usage")
}

証明書の Key Usage に digitalSignature が含まれていることの確認です。

if !hasExtension(ocspResponse.Certificate.Extensions, oidExtensionNameOCSPNoCheck) {
	return errors.New("no id-pkix-ocsp-nocheck extension")
}
func hasExtension(ext []pkix.Extension, oid asn1.ObjectIdentifier) bool {
	for _, ext := range ext {
		if ext.Id.Equal(oid) {
			return true
		}
	}
	return false
}

証明書に id-pkix-ocsp-nocheck extension が含まれていることの確認です。
RFC 6960 Section 4.2.2.2.1. Revocation Checking of an Authorized Responder に記載の通り、この extension が含まれている場合は証明書の有効期間内は OCSP Responder を信用できるものとして扱えるため、失効確認を省略できます。

pool := x509.NewCertPool()
pool.AddCert(issuer)
opts := x509.VerifyOptions{
	Roots:     pool,
	KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning},
}
if _, err := ocspResponse.Certificate.Verify(opts); err != nil {
	return err
}

証明書の検証を行いますが、issuer によって直接署名されている事、EKU (Extended Key Usage) として id-kp-OCSPSigning が指定されている事を検証する必要があります。

OCSP レスポンスの確認

RFC 6960 Section 2.4. Semantics of thisUpdate, nextUpdate, and producedAt

if ocspResponse.ThisUpdate.After(now) {
	return fmt.Errorf("ocsp response thisUpdate(%s) is in the future", ocspResponse.ThisUpdate.Format(time.RFC3339))
}

ThisUpdate が過去であることを確認します。未来の場合はレスポンスが誤っています。

if !ocspResponse.NextUpdate.IsZero() && ocspResponse.NextUpdate.Before(now) {
	return fmt.Errorf("ocsp response nextUpdate(%s) is in the past", ocspResponse.NextUpdate.Format(time.RFC3339))
}

NextUpdate が未来であることを確認します。過去の場合はレスポンスの内容が古いため、信頼性が低いと見なすべきです。
NextUpdate は optional のため、指定されていない場合は無視します。

if ocspResponse.Status != ocsp.Good {
	return errors.New("ocsp response status is not good")
}

OCSP レスポンスのステータスが Good であることを確認します。

最後に

OCSP レスポンスを利用する際には、その前提として OCSP Responder が信用できること、OCSP レスポンスが信用できることが重要です。
Delegated OCSP Responder の場合、特に OCSP Responder の証明書の検証が重要になるため注意が必要です。

参考

脚注
  1. 中間認証局の証明書に OCSP EKU (id-kp-OCSPSigning) を付与してしまうと、中間認証局の証明書が OCSP Responder としての要件を満たしていしまい、ルート認証局の証明書により署名された証明書に関する OCSP レスポンスが生成できてしまう問題があります。 ↩︎

Discussion