Skip to content

实现原理:CAA 与 DNS

对应代码:pkg/caa.go · 入口 CheckCAA(target)

CAA(Certification Authority Authorization,RFC 6844)让域名所有者在 DNS 里声明"只允许哪些 CA 给我签证书"。cert-skills 检查证书的签发 CA 是否被域名 CAA 授权。

为什么手搓 DNS

Go 标准库的 DNS 解析器不认 CAA 记录(type 257),所以 cert-skills 自己拼 DNS 报文走裸 UDP。

DNS 报文构造

dnsQueryCAA(L115)拼 12 字节头 + 查询体:

12 字节 DNS 头

偏移字节含义
0-10xAA 0xBB事务 ID(固定)
2-30x01 0x00flags:标准查询,RD=1
4-50x00 0x01QDCOUNT=1
6-70x00 0x00ANCOUNT=0
8-90x00 0x00NSCOUNT=0
10-110x00 0x00ARCOUNT=0

QNAME 编码(L128-135)

每段前缀 1 字节长度字节,末尾追加 0x00

QTYPE / QCLASS

  • QTYPE = 0x01 0x01(CAA = 257 = 0x0101,大端)
  • QCLASS = 0x00 0x01(IN)

DNS 服务器与超时

  • 默认服务器:["8.8.8.8:53", "1.1.1.1:53", "8.8.4.4:53"]caaDNSServers 可测试覆写)
  • DialTimeout = 3 秒
  • conn.SetDeadline = now + 5 秒
  • 响应 n<12 → "DNS response too short",继续下一个 server
  • 逐个尝试,首个成功即返回

响应解析

skipDNSName 压缩指针处理(L265-280)

解析器只跳过名字、不跟踪指针指向的内容——best-effort 设计。

RDATA 拆解

rtype==257 && len(rdata)>=2 才解析:

go
flag       = rdata[0]
tagLength  = int(rdata[1])
tag        = string(rdata[2 : 2+tagLength])
value      = string(rdata[2+tagLength:])
// 要求 2+tagLength <= len(rdata)

合规判定

checkCAACompliance(L283)的逻辑:

extractCAName(L337-358)

O= 优先,CN= 回退

caaDomainMatches(L363-383)双向子串匹配

双方 ToLower 后,三种匹配任一中即 true:

边界情况需人工复核

双向子串匹配对相似域名(如 letsencrypt.org vs letsencrypt.com)可能误判,命中后请人工复核实际 CAA 记录。

issuewild 未参与校验

issuewildiodef 仅分类收集,不产生 violation——当前实现中 issuewild*. 泛解析但未用于校验逻辑。

输出

go
type CAAResult struct {
    Target       string
    HasCAA       bool
    Records      []CAARecord
    IssuerCA     string
    IsCompliant  bool
    Violations   []string
    Error        string
}

type CAARecord struct {
    Flag uint8
    Tag  string   // issue / issuewild / iodef
    Value string
}

无 CAA 记录时 HasCAA=false, IsCompliant=true(任何 CA 可发行)。

实战意义

CAA 不是绝对防护(能改 DNS 就能改 CAA),但它给 CA 多了一道签发前检查,且 CAA 变更可被监控。

局限

  • DNS 路径信任:UDP DNS 无认证,理论可被中间人改包。生产应走 DoH/DoT(cert-skills 当前用裸 UDP)
  • 模糊匹配:双向子串匹配有边界情况,需人工复核

下一步