certificate-transparency-go用例

文章目录

  • 证书的SCT列表
  • 验证SCT
    • 依赖包
    • 加载证书
    • 初始化log机构信息
    • 离线验证+在线验证

证书的SCT列表

浏览器对证书链的合法性检查通过后,会再检查服务端证书附件里的SCT列表(Signed Certificate Timestamp);
浏览器内置了一批certificate transparency log机构的公钥和访问地址,如果SCT申明证书在某个log机构注册了,但是SCT里的签名通过不了log机构的公钥验证,则抛出错误NET::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED

验证SCT

依赖包

使用github.com/google/certificate-transparency-go工具

import (
	"context"
	"encoding/base64"
	"encoding/pem"
	"errors"
	"io"
	"log"
	"net/http"
	"os"
	"time"

	ct "github.com/google/certificate-transparency-go"
	"github.com/google/certificate-transparency-go/ctutil"
	"github.com/google/certificate-transparency-go/loglist3"
	ctX509 "github.com/google/certificate-transparency-go/x509"
	"github.com/google/certificate-transparency-go/x509util"
)

加载证书

假设服务端证书以及签发该证书的上级CA证书,已保存为PEM格式的文件

func VerifySCT(certLocation string, issuerLocation string) error {
    // 服务端证书
	certByte, err := os.ReadFile(certLocation)
	if err != nil {
		return err
	}
	block, _ := pem.Decode(certByte)
	if block == nil || len(block.Bytes) == 0 {
		return errors.New("error decoding certificate")
	}
	cert, err := ctX509.ParseCertificate(block.Bytes)
	if err != nil {
		return err
	}
	// 上级CA
	certByte, _ = os.ReadFile(issuerLocation)
	block, _ = pem.Decode(certByte)
	if block == nil || len(block.Bytes) == 0 {
		return errors.New("error decoding issuer CA")
	}
	issuer, _ := ctX509.ParseCertificate(block.Bytes)
	// 生成merkle tree leaf,用于验证sct(Signed Certificate Timestamp)
	merkleLeaf, err := ct.MerkleTreeLeafForEmbeddedSCT([]*ctX509.Certificate{cert, issuer}, 0)
	if err != nil {
		return err
	}
	// 获取证书里附带的sct列表
	sctList, err := x509util.ParseSCTsFromSCTList(&cert.SCTList)
	if err != nil {
		log.Printf("ParseCertificate failed %v", err)
		return err
	}
	log.Printf("验证证书%s的SCT列表", cert.Subject)

初始化log机构信息

使用和chrome一致的机构列表:https://www.gstatic.com/ct/log_list/v3/log_list.json

	// 获取chrome使用的certificate transparency log机构列表,包含机构使用的公钥和查询api地址
	resp, err := http.DefaultClient.Get(loglist3.LogListURL)
	if err != nil {
		return errors.New("下载certificate transparency log地址列表失败")
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return errors.New("下载certificate transparency log地址列表失败")
	}
	loglistEntry, _ := loglist3.NewFromJSON(body)
	logsByHash, _ := ctutil.LogInfoByKeyHash(loglistEntry, http.DefaultClient)

离线验证+在线验证

logInfo.VerifySCTSignature方法不需要和log机构在线交互,是使用已知的log机构公钥对SCT进行离线验证

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	for _, sct := range sctList {
		// 验证sct,参考https://github.com/google/certificate-transparency-go/blob/master/ctutil/sctscan/sctscan.go
		log.Printf("sct signature: %s, %s", base64.StdEncoding.EncodeToString(sct.Signature.Signature), time.Unix(0, int64(sct.Timestamp)*int64(time.Millisecond)).Format(time.RFC3339Nano))
		logInfo, ok := logsByHash[sct.LogID.KeyID]
		if !ok {
			log.Printf("sct key_hash: %s,不存在对应certificate transparency log机构", base64.StdEncoding.EncodeToString(sct.LogID.KeyID[:]))
			continue
		}
		log.Printf("颁发sct的certificate transparency log机构是: %s,地址:%s, 公钥哈希:%s", logInfo.Description,
			logInfo.Client.BaseURI(), base64.StdEncoding.EncodeToString(sct.LogID.KeyID[:]))
		err = logInfo.VerifySCTSignature(*sct, *merkleLeaf)
		if err != nil {
			log.Printf("Verify SCT failed %v", err)
			continue
		}
		log.Println("Verify SCT offline OK")
		// 线上验证,非必须
		if _, err := logInfo.VerifyInclusionLatest(ctx, *merkleLeaf, sct.Timestamp); err != nil {
			sth := logInfo.LastSTH()
			if sth != nil {
				delta := time.Duration(sth.Timestamp-sct.Timestamp) * time.Millisecond
				if delta < logInfo.MMD {
					// 如果生效时间(logInfo.MMD)还未到,那么机构查询不到该sct的merkle tree leaf信息是正常的
					log.Printf("SCT's MMD has not passed %d -> %d < %v", sct.Timestamp, sth.Timestamp, logInfo.MMD)
					continue
				}
			}
			log.Printf("Failed to verify SCT online: %v", err)
		} else {
			log.Println("Verify SCT online OK")
		}
	}

每个SCT分别是不同log机构签发的,如果一个证书附带的两个SCT是由同一个log机构签发,浏览器似乎也会报错