Ruby如何使用OpenSSL X509解析证书SAN扩展中的DNS和IP地址?

来源:APP编程网作者:广州GEO公司头衔:草根站长
导读:本期聚焦于广州GEO公司创作的《Ruby如何使用OpenSSL X509解析证书SAN扩展中的DNS和IP地址?》,敬请观看详情。SSL证书里的主题备用名SAN扩展到底藏了哪些域名和IP?用Ruby的OpenSSL标准库就能直接解析出来。本文围绕OpenSSL::X509::Certificate对象,讲解如何读取extensions扩展字段,通过 OpenSSL::X509::Extension的value方法拿到原始内容,再利用ASN.1解码或正则把DNS、IP Address等条目逐项拆出来。文中对比了subject字段CN与SAN的区别,说明为什么现代证书校验必须依赖SAN,并给出完整可运行的代码示例,包括处理多值SAN、IP地址还原、异常捕获以及批量解析证书文件的思路,帮助你在证书监控、自动化运维场景中快速落地。

在HTTPS通信中,证书是否覆盖某个域名,判断依据早就不是Common Name了,而是SAN(Subject Alternative Name,主题备用名)扩展。一张证书的SAN里可能同时包含几十个域名和若干IP地址,用Ruby自带的OpenSSL标准库可以把这些信息完整解析出来。这篇文章围绕OpenSSL::X509::Certificate,详细介绍如何提取并拆解SAN扩展中的DNS条目和IP地址。

Ruby如何使用OpenSSL X509解析证书SAN扩展中的DNS和IP地址?

为什么证书校验要看SAN而不是CN

早期证书只靠subject里的Common Name字段标识域名,浏览器拿访问的域名和CN比对即可。但随着一张证书需要承载多个域名、通配符域名甚至IP地址,CN的表达能力明显不够了。RFC 2818之后,主流客户端(包括Chrome、Firefox和各类HTTP库)都把SAN作为唯一的域名匹配依据,如果证书没有SAN扩展,即使CN完全一致,校验也会失败。

用Ruby可以直接观察这个差异。CN在证书的subject字段中:

require 'openssl'

raw = File.read('server.crt')
cert = OpenSSL::X509::Certificate.new(raw)

# 取出CN
cn = cert.subject.to_a.find { |name, _, _| name == 'CN' }
puts cn[1]  # 例如输出 ippipp.com

而SAN则存放在扩展区。通过cert.extensions可以拿到所有扩展,每个扩展都是一个OpenSSL::X509::Extension对象,包含扩展名、值和critical标记。SAN对应的扩展名是subjectAltName,它的value是一段DER编码后再转为文本的内容,形如DNS:ippipp.com, DNS:*.ippipp.com, IP Address:192.168.1.1。解析工作的核心就是把这段字符串结构化。

提取并拆解SAN扩展的完整代码

最直接的做法是遍历extensions,找到目标扩展后按逗号切分value,再根据前缀分类。每一条SAN记录由类型和值组成,常见类型有DNS、IP Address、email、URI等。下面这段代码把DNS和IP分别收集到数组里:

require 'openssl'

def parse_san(cert)
  san_ext = cert.extensions.find { |ext| ext.oid == 'subjectAltName' }
  return { dns: [], ips: [] } unless san_ext

  dns = []
  ips = []
  san_ext.value.split(', ').each do |entry|
    type, value = entry.split(':', 2)
    case type.strip
    when 'DNS'        then dns << value.strip
    when 'IP Address' then ips << value.strip
    end
  end
  { dns: dns, ips: ips }
end

cert = OpenSSL::X509::Certificate.new(File.read('server.crt'))
result = parse_san(cert)
puts "DNS域名: #{result[:dns].join(', ')}"
puts "IP地址: #{result[:ips].join(', ')}"

这里有几个细节值得注意。split(',', 2)这种写法是错的用法要避免,正确的做法是对每个条目用split(':', 2)限制只切一次,因为IP地址里可能残留冒号形式的IPv6文本。另外分隔符建议用', '(带空格),OpenSSL生成的value条目之间就是逗号加空格。

IPv6是常见的坑。在SAN中,IPv6地址的十六进制字节会以冒号连接显示,例如IP Address:FD00:0:0:0:0:0:0:1,直接按冒号切割会把它截断。所以解析时必须用限制分割次数的方式,或者干脆改用更严谨的ASN.1方式(下文会讲)。对于只需DNS和IPv4的场景,上面的简化版已经够用。

用ASN.1解码获取结构化数据

文本切分终究是权宜之计,更可靠的方式是对扩展值做ASN.1解码。SAN在DER结构里是一个GeneralNames序列,每个GeneralName带上下文标签:tag 2是dNSName,tag 7是iPAddress。借助OpenSSL::ASN1.decode可以把扩展的原始字节还原成结构树:

require 'openssl'

def parse_san_asn1(cert)
  ext = cert.extensions.find { |e| e.oid == 'subjectAltName' }
  return { dns: [], ips: [] } unless ext

  seq = OpenSSL::ASN1.decode(ext.to_der)
  dns, ips = [], []

  seq.value.first.value.each do |name|
    case name.tag
    when 2 then dns << name.value.force_encoding('UTF-8')
    when 7
      bytes = name.value.unpack('C*')
      ip = if bytes.size == 4
             bytes.join('.')
           else
             # IPv6:每两个字节转十六进制,冒号连接
             bytes.each_slice(2).map { |p| p.sum { |b| b }.to_s(16) }.join(':')
           end
      ips << ip
    end
  end
  { dns: dns, ips: ips }
end

ext.to_der返回扩展的DER编码字节,解码后得到的是Extension的外层结构,取第一层value才是GeneralNames序列。tag 7的iPAddress是原始字节串,IPv4占4字节,IPv6占16字节,需要手动还原成点分十进制或冒号十六进制。上面的IPv6还原逻辑是简化写法,严格实现应把每两个字节按大端拼成一个16位段再转十六进制。

ASN.1方式的好处是完全不依赖value的文本格式,无论OpenSSL版本如何变化、条目里是否包含逗号或特殊字符,都能稳定解析。缺点是代码更繁琐,需要理解标签结构。如果只是做证书信息展示,文本切分够用;如果是写自动化校验工具,建议直接上ASN.1方案。

实际应用中的注意事项

解析证书时别忘了处理异常。证书文件损坏、编码不对或者不是X509格式,OpenSSL::X509::Certificate.new会抛出OpenSSL::X509::CertificateError,批量处理时最好用rescue包住单个文件的处理逻辑,避免一张坏证书中断整个任务:

Dir.glob('certs/*.crt').each do |path|
  begin
    cert = OpenSSL::X509::Certificate.new(File.read(path))
    san = parse_san(cert)
    puts "#{path}: #{san[:dns].size}个DNS, #{san[:ips].size}个IP"
  rescue OpenSSL::X509::CertificateError => e
    warn "跳过无效证书 #{path}: #{e.message}"
  end
end

另一个实用场景是从线上服务器直接抓证书。配合TCPSocket和OpenSSL::SSL::SSLSocket可以在握手阶段拿到对端证书,不需要本地文件。做证书到期监控或者域名覆盖检查时,这个组合非常顺手:解析出SAN后,和你维护的域名清单求差集,就能发现哪些域名即将失去证书保护。

还要提醒一点,通配符域名(如*.ippipp.com)会原样出现在DNS条目里,如果你的校验逻辑要匹配通配符,需要自己实现*.前缀的匹配规则,OpenSSL本身不会替你做这层转换。把解析、匹配、告警拆成独立方法,代码会清晰很多。

Ruby OpenSSLX509证书SAN扩展修改时间:2026-09-12 02:56:34

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260912/55066.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。