Skip to content

实现原理:证书变更检测

对应代码:pkg/certchange.go · 入口 TakeSnapshot / DetectChange

detect-change 通过对比证书快照判断"变了什么、变成什么类型"。本页讲快照结构、7 步对比顺序与变更类型推断。

快照结构

go
type CertSnapshot struct {
    Target       string
    Timestamp    time.Time
    CertSHA256   string            // 整张证书哈希
    SPKISHA256   string            // 公钥哈希(续期不变)
    Issuer       string
    NotBefore    time.Time
    NotAfter     time.Time
    SerialNumber string
    JARMHash     string            // omitempty
    Metadata     map[string]string // omitempty
}

TakeSnapshot(L61-97)字段填充:

  • cert.Fingerprints["sha256"] → CertSHA256
  • ["public_key_sha256"] → SPKISHA256
  • JARM 走 JARMScan,失败不致命(best effort)
  • ErrCertNotFound 当 certs 为空

7 步对比顺序

DetectChange(L100-171)逐字段比对:

变更类型推断

场景CertSHA256SPKI类型
续期(同公钥新证)不变renewed
换钥/换 CAreplaced
无变化不变不变unchanged
过期expired(叠加)
首次new

renewed / replaced / expired 的判定关系

renewedreplaced 互斥,由 SPKI 是否相同决定。expired叠加判定,不覆盖已设的 renewed/replaced——所以一张证书可以同时是 renewed + expired

核心洞察:用 SPKI 区分"续期"和"换钥"——续期(同公钥、新有效期/新签名)时整张证书变了但公钥没变,SPKI 不变。这正是 SPKI 比 cert SHA-256 更适合"轮换检测"的原因,详见 核心概念·指纹

持久化:SnapshotStore

文件名格式(L184)

<sanitizeFilename(snap.Target)>_<snap.Timestamp.Format("20060102_150405")>.json

例:example_com_20260703_144015.json

ComputeSnapshotID 输入串(L237-244)

ComputeSnapshotID 不含易变字段

ComputeSnapshotID 只哈希 Target + CertSHA256 + SPKISHA256 + SerialNumber不含 timestamp、issuer、NotBefore/NotAfter、JARM——所以同一证书多次快照会得到相同 ID。这适合做"同一证书"的去重键,不适合做"每次快照"的唯一键。

输出

go
type CertChangeResult struct {
    HasChanged    bool
    ChangeType    string         // new/renewed/replaced/expired/unchanged
    CurrentSnap   *CertSnapshot
    PreviousSnap  *CertSnapshot
    Changes       []ChangeDetail
}

CertChangeRecord 还预留 revoked 类型——未来可结合 CRL 检测吊销态变化。

与时间线的串联

map-timeline 消费多个快照 JSON,构建完整生命周期。这是 网络空间测绘扫描工作流 的"追踪轮换"阶段。

下一步