Skip to content

实现原理:18 项证书检查

对应代码:pkg/certvulnscan.go(在线 ScanCertSecurity)+ pkg/offline.go(离线 ScanCertSecurityFromChain

scan-cert-security 跑的 18 项 CERT-001 ~ CERT-018 是怎么实现的?本页给完整检查表、精确阈值与 state 跳过逻辑。

架构:表驱动检查

每个 certCheck 结构体:

go
type certCheck struct {
    name     string
    code     string  // "CERT-001"
    severity string  // Critical/High/Medium/Low
    desc     string
    check    func(cert *x509.Certificate, host string,
                 state *tls.ConnectionState) (passed bool, detail string)
}

切片顺序即 CERT-001..018,每项返回 (Passed, Detail)。表驱动让每项独立可测、易扩展。

18 项完整检查表

名称严重度check 函数阈值/判定
CERT-001Weak Signature AlgorithmHighcheckWeakSignature算法 ∈ {MD5, SHA1, MD2}
CERT-002Short RSA KeyHighcheckShortRSAKeyRSA <2048
CERT-003Weak ECDSA CurveMediumcheckWeakCurveBitSize<256(P-224 判弱)
CERT-004Missing SANHighcheckMissingSANDNSNames 为空
CERT-005Hostname MismatchCriticalcheckHostnameMismatchcert.VerifyHostname(host) 失败
CERT-006Excessive Validity PeriodMediumcheckExcessiveValidity>398
CERT-007Self-Signed CertificateMediumcheckSelfSignedSubject==Issuer
CERT-008Certificate ExpiredCriticalcheckCertExpiredNotAfter < now
CERT-009Certificate Expiring SoonHighcheckCertExpiringSoon0 < 剩余 <= 30
CERT-010CN Not in SANsLowcheckCNNotInSANsCommonNameDNSNames
CERT-011Wildcard CertificateLowcheckWildcardCertSAN 含 *.
CERT-012Internal Name CertificateHighcheckInternalNameSAN 含 10 个内部 TLD 之一
CERT-013Untrusted ChainHighcheckUntrustedChain系统根池 Verify 失败
CERT-014Distrusted CACriticalcheckDistrustedCA命中 DigiNotar/WoSign 等清单
CERT-015OCSP Must-Staple ViolationHighcheckOCSPMustStapleRFC 7633 扩展在但 OCSP staple 空
CERT-016Key Usage Non-ComplianceHighcheckKeyUsageCompliance见下文 4 规则
CERT-017Low Serial EntropyMediumcheckSerialEntropyBitLen<64 或 Shannon 熵 <3.0
CERT-018Name Constraint ViolationHighcheckNameConstraints叶名越 CA 的 permitted/excluded

12 + 6 分组

state==nil 跳过逻辑

ScanCertSecurityFromChain(offline.go L299-309)的核心分支:

go
checks = append(checks, certChecks[:12]...)   // 前 12 项常驻
if state != nil {
    checks = append(checks, certChecks[12:]...)  // 后 6 项需 state
}

这就是为什么离线文件分析也能给出 12 项证书层结果——后 6 项依赖 state.PeerCertificates / state.OCSPResponse,离线无 state 时严格跳过。

几个判定细节

自签判定(CERT-007)

不是看 IsCA,而是直接比 Subject 与 Issuer 的 DER 字节:

go
selfSigned := bytes.Equal(cert.RawSubject, cert.RawIssuer) &&
             bytes.Equal(cert.RawSubjectPublicKeyInfo, cert.RawAuthorityKeyId)

内部名判定(CERT-012)

内置 10 个内部 TLD 后缀:

.local .internal .intranet .private .corp .home .lan .test .example .invalid

SAN 命中任一即告警——公网证书不该用这些。

序列号熵(CERT-017)

三维度同时判:

isSequentialSerial(serialentropy.go L162-200)三判据:serial<1000 / 是 10^1..10^20 的倍数 / 高位字节(除末 8 字节)全零且 leadingZeros > len(bytes)-8

Key Usage 4 规则(CERT-016)

四条规则每条 Severity="High"。

Must-Staple 判定(CERT-015)

hasMustStapleExtension(ocspmuststaple.go L89-111)按 OID 1.3.6.1.5.5.7.1.24 双重匹配:先逐字节比 Id[0..8],再字符串比 ext.Id.String()。命中扩展但 state.OCSPResponse 为空即违规。

Must-Staple 判定可能误报

hasStatusRequestInValue 用宽松的 best-effort 判定(找 02 01 05 字节序列,找不到退化为"任何 0x05 字节"),可能误报。生产环境确认 Must-Staple 请用 check-ocsp-must-staple 命令交叉验证。

不可信 CA 判定(CERT-014)

matchDistrustedCA(distrustedca.go L209-256)对 cert 的 Subject.CNSubject.Organization[0]Issuer.CNIssuer.Organization[0]、full Subject、full Issuer 与内置 distrustedCAs 表做 strings.Contains 匹配,任中即返回 {Name, Subject, Reason, DistrustDate, Severity}

离线评分的独立权重

离线版把未通过检查映射成 issue(offline.go L55-64):

go
issue.Severity = check.Severity
issue.Type     = check.Code      // 如 "CERT-005"
issue.Description = check.Name
issue.Impact      = check.Detail

然后用离线专属权重(详见 安全评分算法 对比表)算分。

下一步