文章目录
- 证书的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机构签发,浏览器似乎也会报错