浏览器对证书链的合法性检查通过后,会再检查服务端证书附件里的SCT列表(Signed Certificate Timestamp);
浏览器内置了一批certificate transparency log机构的公钥和访问地址,如果SCT申明证书在某个log机构注册了,但是SCT里的签名通过不了log机构的公钥验证,则抛出错误NET::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED
使用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)
err = cert.CheckSignatureFrom(issuer)
if err != nil {
log.Printf("证书%s的签名算法是%s,CA签名没有验证成功", cert.Subject, cert.SignatureAlgorithm.String())
return err
}
// 生成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)
使用和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机构签发,或者SCT列表涉及的log机构合计不足3个,浏览器似乎也会报错;
目前,证书检查通过后,chrome的F12里才显示解析的SCT列表;ERR_CERTIFICATE_TRANSPARENCY_REQUIRED错误发生时,需要使用这个程序来查看SCT列表里是否存在重复注册、注册的log机构数量不足,甚至是否无法通过SCT签名检查