はじめに
Go言語でPKI入門(勝手に復刻版)
の続編です。今回は、証明書の失効に関連したCRLとOCSPに話です。
証明書を失効するとは?
CA(認証局)が発行した証明書を失効するというのは証明書のファイルを何か変更するわけではありません。
CAの中で、その証明書を失効したとマークを付けるだけです。ファイルやデータベースに失効した日時を記録すればよいだけです。
なので、証明書だけ見ても有効なのか失効しているのかわかりません。CAに聞かなかればなりません。CAに聞く方法が2つあります。CRLとOCSPです。
他力ですが、CRLとOCSPの説明は
がわかりやすいと思います。
CRL(証明書失効リスト)を作る
Go言語でCRLを作るには
// 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を作成できます。失効した証明書はシリアル番号3にしています。
そのまま、保存すればDER形式、上の例ではPEM形式で保存しています。
このファイルをHTTPサーバーからダウンロードできるようにして配布します。定期的にダウンロードして証明書の失効をチェックすることになります。
結構たいへんです。
OCSP(Online Certificate Status Protocol)
CRLよりましにしたのがOCSPです。証明書の状態を問い合わせるプロトコルです。
基本的はHTTPサーバーで実現します。(HTTPSの必要はないです。)
Go言語でOCSPサーバーを実現するのは
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
を使っています。これは便利です。
OCSPのプロトコルは
のパッケージが使えます。
リクエストの解析と応答の作成ができます。
OCSPはHTTPのGETリクエストまたは、POSTリクエストで送信されます。
両方に対応できるようになっています。データを取り出す場所が違います
isCertRevokeed
関数の中で、証明書の失効を判断しています。上の例ではシリアル番号が3を失効
としています。きちんと実装する時は、DBなどを検索して判断すればよいです。
余談
CRLの作成やOCSPサーバーは、
に組み込むために作っています。