Go言語でPKI入門（勝手に復刻版）：CRL/OCSP編

# はじめに

Go言語でPKI入門（勝手に復刻版）

https://qiita.com/twsnmp/items/60ba568f0daca8bae915

の続編です。今回は、証明書の失効に関連したCRLとOCSPに話です。


# 証明書を失効するとは？

CA（認証局）が発行した証明書を失効するというのは証明書のファイルを何か変更するわけではありません。
CAの中で、その証明書を失効したとマークを付けるだけです。ファイルやデータベースに失効した日時を記録すればよいだけです。

なので、証明書だけ見ても有効なのか失効しているのかわかりません。CAに聞かなかればなりません。CAに聞く方法が２つあります。CRLとOCSPです。

他力ですが、CRLとOCSPの説明は

https://www.infraexpert.com/study/sslserver13.html

がわかりやすいと思います。


# CRL(証明書失効リスト)を作る

Go言語でCRLを作るには

```go
// CRL
// RFC5280, 5.2.5
type issuingDistributionPoint struct {
	DistributionPoint          distributionPointName `asn1:"optional,tag:0"`
	OnlyContainsUserCerts      bool                  `asn1:"optional,tag:1"`
	OnlyContainsCACerts        bool                  `asn1:"optional,tag:2"`
	OnlySomeReasons            asn1.BitString        `asn1:"optional,tag:3"`
	IndirectCRL                bool                  `asn1:"optional,tag:4"`
	OnlyContainsAttributeCerts bool                  `asn1:"optional,tag:5"`
}

type distributionPointName struct {
	FullName     []asn1.RawValue  `asn1:"optional,tag:0"`
	RelativeName pkix.RDNSequence `asn1:"optional,tag:1"`
}

func CreateCRL() {
	dp := distributionPointName{
		FullName: []asn1.RawValue{
			{Tag: 6, Class: 2, Bytes: []byte("http://127.0.0.1:5585/crl")},
		},
	}
	var oidExtensionIssuingDistributionPoint = []int{2, 5, 29, 28}

	idp := issuingDistributionPoint{
		DistributionPoint: dp,
	}

	v, err := asn1.Marshal(idp)
	if err != nil {
		panic(err)
	}

	cdpExt := pkix.Extension{
		Id:       oidExtensionIssuingDistributionPoint,
		Critical: true,
		Value:    v,
	}

	key, ok := rootCAPrivateKey.(crypto.Signer)
	if !ok {
		panic("invalid key type")
	}
	ca, err := x509.ParseCertificate(rootCACertificate)
	if err != nil {
		panic(err)
	}
	revokedCerts := []pkix.RevokedCertificate{
		{
			SerialNumber:   big.NewInt(3),
			RevocationTime: time.Now(),
		},
	}
	crlTpl := &x509.RevocationList{
		SignatureAlgorithm:  x509.ECDSAWithSHA256,
		RevokedCertificates: revokedCerts,
		Number:              big.NewInt(2),
		ThisUpdate:          time.Now(),
		NextUpdate:          time.Now().Add(24 * time.Hour),
		ExtraExtensions:     []pkix.Extension{cdpExt},
	}
	crl, err = x509.CreateRevocationList(rand.Reader, crlTpl, ca, key)
	if err != nil {
		panic(err)
	}
	OutPem(crl, "./rootCA.crl", "X509 CRL")
}

```
のようなコードになります。
必要なものは

- CAの秘密鍵と証明書
- CRLを配布するURL
- 失効した証明書のリスト（シリアル番号と失効した日時）

です。これらを`x509.RevocationList`にまとめてx509.CreateRevocationList関数に渡せば、
CLRを作成できます。失効した証明書はシリアル番号３にしています。
そのまま、保存すればDER形式、上の例ではPEM形式で保存しています。

このファイルをHTTPサーバーからダウンロードできるようにして配布します。定期的にダウンロードして証明書の失効をチェックすることになります。
結構たいへんです。

# OCSP(Online Certificate Status Protocol)

CRLよりましにしたのがOCSPです。証明書の状態を問い合わせるプロトコルです。
基本的はHTTPサーバーで実現します。（HTTPSの必要はないです。）

Go言語でOCSPサーバーを実現するのは

```go
package main

import (
	"bytes"
	"crypto"
	"crypto/sha1"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"io"
	"log"
	"net/http"
	"time"

	"github.com/labstack/echo/v4"
	"golang.org/x/crypto/ocsp"
)

// OCSP Server
func OCSPServer() {
	e := echo.New()
	e.GET("/crl", func(c echo.Context) error {
		log.Printf("get crl %+v", c)
		c.Response().Header().Add(echo.HeaderCacheControl, "max-age=0, no-cache")
		return c.Blob(http.StatusOK, "application/pkix-crl", crl)
	})
	e.GET("/ocsp/:req", func(c echo.Context) error {
		req := c.Param("req")
		b, err := base64.StdEncoding.DecodeString(req)
		if err != nil {
			log.Printf("err=%v", err)
			return c.JSON(http.StatusBadRequest, err)
		}
		return ocspFunc(c, b)
	})
	e.POST("/ocsp", func(c echo.Context) error {
		b, err := io.ReadAll(c.Request().Body)
		if err != nil {
			log.Printf("err=%v", err)
			return c.JSON(http.StatusBadRequest, err)
		}
		return ocspFunc(c, b)
	})
	e.Logger.Fatal(e.Start("127.0.0.1:5585"))
}

func ocspFunc(c echo.Context, b []byte) error {
	req, err := ocsp.ParseRequest(b)
	if err != nil {
		log.Printf("err=%v", err)
		return c.JSON(http.StatusBadRequest, err)
	}
	log.Printf("post ocsp %+v", req)
	key, ok := rootCAPrivateKey.(crypto.Signer)
	if !ok {
		log.Printf("err=%v", err)
		return c.JSON(http.StatusBadRequest, err)
	}
	ca, err := x509.ParseCertificate(rootCACertificate)
	if err != nil {
		log.Printf("err=%v", err)
		return c.JSON(http.StatusBadRequest, err)
	}
	res := ocsp.Response{
		Status:       ocsp.Unknown,
		SerialNumber: req.SerialNumber,
		ThisUpdate:   time.Now(),
		NextUpdate:   time.Now().Add(time.Second * 60 * 10),
		IssuerHash:   req.HashAlgorithm,
	}
	if bytes.Equal(req.IssuerKeyHash, ca.SubjectKeyId) {
		sn := req.SerialNumber.String()
		if isCertRevokeed(sn) {
			res.Status = ocsp.Revoked
			res.RevokedAt = time.Now().Add(time.Hour * -3)
			res.RevocationReason = ocsp.Unspecified
		} else {
			res.Status = ocsp.Good
		}
	}
	bres, err := ocsp.CreateResponse(ca, ca, res, key)
	if err != nil {
		log.Printf("err=%v", err)
		return c.JSON(http.StatusBadRequest, err)
	}
	return c.Blob(http.StatusOK, "application/ocsp-response", bres)
}

func HashedBySha1(b []byte) string {
	sha1 := sha1.New()
	sha1.Write(b)
	return hex.EncodeToString(sha1.Sum(nil))
}

func isCertRevokeed(sn string) bool {
	return sn == "3"
}

```

のような感じになります。
先ほど作成したCRLの配布も実装しています。

HTTPサーバーは、Echo

https://echo.labstack.com/

を使っています。これは便利です。

OCSPのプロトコルは

https://pkg.go.dev/golang.org/x/crypto/ocsp

のパッケージが使えます。
リクエストの解析と応答の作成ができます。

OCSPはHTTPのGETリクエストまたは、POSTリクエストで送信されます。
両方に対応できるようになっています。データを取り出す場所が違います

`isCertRevokeed`関数の中で、証明書の失効を判断しています。上の例ではシリアル番号が３を失効
としています。きちんと実装する時は、DBなどを検索して判断すればよいです。


# 余談

CRLの作成やOCSPサーバーは、

https://github.com/twsnmp/twsnmpfk

に組み込むために作っています。