实现原理:18 项证书检查
对应代码:
pkg/certvulnscan.go(在线ScanCertSecurity)+pkg/offline.go(离线ScanCertSecurityFromChain)
scan-cert-security 跑的 18 项 CERT-001 ~ CERT-018 是怎么实现的?本页给完整检查表、精确阈值与 state 跳过逻辑。
架构:表驱动检查
每个 certCheck 结构体:
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-001 | Weak Signature Algorithm | High | checkWeakSignature | 算法 ∈ {MD5, SHA1, MD2} |
| CERT-002 | Short RSA Key | High | checkShortRSAKey | RSA <2048 |
| CERT-003 | Weak ECDSA Curve | Medium | checkWeakCurve | BitSize<256(P-224 判弱) |
| CERT-004 | Missing SAN | High | checkMissingSAN | DNSNames 为空 |
| CERT-005 | Hostname Mismatch | Critical | checkHostnameMismatch | cert.VerifyHostname(host) 失败 |
| CERT-006 | Excessive Validity Period | Medium | checkExcessiveValidity | >398 天 |
| CERT-007 | Self-Signed Certificate | Medium | checkSelfSigned | Subject==Issuer |
| CERT-008 | Certificate Expired | Critical | checkCertExpired | NotAfter < now |
| CERT-009 | Certificate Expiring Soon | High | checkCertExpiringSoon | 0 < 剩余 <= 30 天 |
| CERT-010 | CN Not in SANs | Low | checkCNNotInSANs | CommonName ∉ DNSNames |
| CERT-011 | Wildcard Certificate | Low | checkWildcardCert | SAN 含 *. |
| CERT-012 | Internal Name Certificate | High | checkInternalName | SAN 含 10 个内部 TLD 之一 |
| CERT-013 | Untrusted Chain | High | checkUntrustedChain | 系统根池 Verify 失败 |
| CERT-014 | Distrusted CA | Critical | checkDistrustedCA | 命中 DigiNotar/WoSign 等清单 |
| CERT-015 | OCSP Must-Staple Violation | High | checkOCSPMustStaple | RFC 7633 扩展在但 OCSP staple 空 |
| CERT-016 | Key Usage Non-Compliance | High | checkKeyUsageCompliance | 见下文 4 规则 |
| CERT-017 | Low Serial Entropy | Medium | checkSerialEntropy | BitLen<64 或 Shannon 熵 <3.0 |
| CERT-018 | Name Constraint Violation | High | checkNameConstraints | 叶名越 CA 的 permitted/excluded |
12 + 6 分组
state==nil 跳过逻辑
ScanCertSecurityFromChain(offline.go L299-309)的核心分支:
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 字节:
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 .invalidSAN 命中任一即告警——公网证书不该用这些。
序列号熵(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.CN、Subject.Organization[0]、Issuer.CN、Issuer.Organization[0]、full Subject、full Issuer 与内置 distrustedCAs 表做 strings.Contains 匹配,任中即返回 {Name, Subject, Reason, DistrustDate, Severity}。
离线评分的独立权重
离线版把未通过检查映射成 issue(offline.go L55-64):
issue.Severity = check.Severity
issue.Type = check.Code // 如 "CERT-005"
issue.Description = check.Name
issue.Impact = check.Detail然后用离线专属权重(详见 安全评分算法 对比表)算分。
下一步
- 评分怎么用这些检查结果 → 安全评分算法
- 不可信 CA 清单 → 合规检查
- 离线分析 → 离线 vs 在线分析