Web Security – Ceritificate

Table of Contents

1. Concepts

1) 密钥对(私钥 & 公钥)

What

  • 私钥:只属于持有者的秘密数据;用于签名解密
  • 公钥:可公开分发;用于验签加密
  • 常见算法:RSA(2048/3072/4096)、ECDSA(P-256/384/521)、Ed25519(签名友好、体积小)。

Why

  • 将“机密性”和“身份/完整性”分离:公开分发公钥不会泄密;只有私钥才能产生可信签名。
  • 支持大规模、跨组织的互联网信任模型(PKI)。

When

  • TLS/HTTPS、mTLS、JWT 签名、软件发布签名(如容器镜像/包)、SSH 登录等。

Who

  • 服务器/服务实例、反向代理、客户端(mTLS)、CI/CD 签名者、KMS/HSM 托管方、开发者/运维。

How(要点 & 实践)

  • 生成与存放:优先 KMS/HSM 或文件系统 + 严格权限;避免将私钥写入镜像与代码库。
  • 算法选择:

    • 兼容性优先:RSA-2048;
    • 性能与体积:ECDSA P-256;
    • 现代签名:Ed25519(但看 TLS/库兼容性)。
  • 备份与轮换:定期轮换(如 6–12 个月),并保留旧私钥用于历史数据验签。

2) 数字签名(Digital Signature)

What

  • 私钥对“数据的哈希”做签名(即用私钥对这个摘要进行加密),任何人用公钥可验证“是谁签的、有没有被改”。

Why

  • 证明 身份(只有私钥持有者能签)、保证 完整性(数据改了就验不过)。

When

  • CA 签发证书;TLS 握手;JWT/消息/工件(artifact)签名;代码签名。

Who

  • 签名者:持私钥方(服务器、CA、CI/CD)。
  • 验签者:浏览器、服务端、消费者。

How(流程)

  1. 对数据做哈希(SHA-256 等)。
  2. 私钥对哈希签名(加密) → 产生签名值。
  3. 验签时:公钥还原签名(解密)并比对哈希是否一致。
  4. TLS/证书里,验签逻辑由 x509.Verify、浏览器/系统完成。

3) CSR(证书签名请求)

What

  • 包含 申请者公钥 + 主题信息(CN/组织) + 扩展(SAN) 的请求文件,本身由申请者私钥签名。

Why

  • CA 通过 CSR 确认:

    • 你拥有这把私钥;
    • 你声明的身份/域名是什么。

When

  • 向公有 CA(Let’s Encrypt/DigiCert)或内部 CA 申请证书。

Who

  • 申请证书的主体(网站/服务),或自动化工具(cert-manager、lego、acme.sh)。

How(关键点)

  • 在生成 CSR 时就把 SAN 写全(DNS/IP/URI)。
  • 保护私钥;CSR 可公开。
  • 通过 ACME/HTTP-01/DNS-01 完成域名所有权验证(公有 CA)。

4) 证书(X.509)与 SAN

What

  • 证书 = 公钥 + 身份信息 + 有效期 + 扩展(KeyUsage/EKU/SAN/AIA/AKI/SKI),由 CA 私钥签名
  • SAN(Subject Alternative Name):列出证书覆盖的所有主机名/IP/URI。现代客户端仅看 SAN,不再依赖 CN。

Why

  • 将“是谁(身份)”与“这把公钥”绑定;SAN 保障一个证书可安全覆盖多个域名。

When

  • Web 服务器证书、mTLS 客户端证书、内网服务网格证书。
  • 通配符:*.example.com 覆盖单层子域,场景合适时减少证书数量。

Who

  • 颁发者:CA(中级 CA 为主)。
  • 使用者:服务器/客户端;验证者:浏览器、反向代理、API 客户端。

How(实务)

  • KeyUsage/EKU:

    • 服务器证书:DigitalSignature|KeyEncipherment + ExtKeyUsageServerAuth
    • 客户端证书:ExtKeyUsageClientAuth
    • CA:CertSign|CRLSignIsCA=true
  • 有效期:公有 TLS 证书常见 ≤ 398 天;内部可更短以便自动轮换。
  • 一定部署完整链(leaf + intermediates),否则部分客户端“链不完整”。

5) CA / 根 CA / 中级 CA

What

  • CA:受信的签发机构。
  • 根 CA(Root):自签,公钥预置在系统/浏览器;几乎不直接签发终端证书。
  • 中级 CA(Intermediate):由 Root 或上级中级签发,真正负责面向终端的证书签发。

Why

  • 分层降低风险:一旦中级泄露可吊销该中级,不伤根。
  • 运营与审计职责分离,满足合规。

When

  • 互联网 PKI 必备;企业内部 PKI 也建议采用“根离线 + 多中级”架构。

Who

  • 根 CA 运维团队(通常离线 HSM、实体安保);
  • 中级 CA 运营(在线签发、审计、速率限制、策略)。

How(落地)

  • 根 CA 置于离线/受控环境,使用 HSM;
  • 中级 CA 在线服务,配合审批/ACME/审计与告警;
  • 定期轮换中级;根 CA 更长周期轮换;使用 CRL/OCSP 发布状态。

6) 证书链与验证

What

  • 链 = 服务器证书 ← 中级 CA 证书 ← 根 CA 证书(信任锚)。
  • 验证 = 逐级验签 + 策略检查(有效期、KeyUsage/EKU、SAN/主机名、吊销)。

Why

  • 让任意两方在不预先交换私钥的情况下,基于公共“信任锚”(Root)建立信任。

When

  • 浏览器访问 HTTPS、反向代理到上游、服务间 mTLS、API 客户端校验。

Who

  • 验证者:客户端/浏览器/网关/SDK;
  • 提供链:服务器必须连同中级证书一起发送。

How(步骤)

  1. 使用上一级证书的公钥验下一级证书签名
  2. 检查证书有效期KeyUsage/EKU 是否允许当前用途;
  3. 校验 SAN 是否覆盖访问的主机名;
  4. 检查 吊销(CRL/OCSP/OCSP-Stapling);
  5. 根 CA 作为信任锚存在于本机信任库中(系统/浏览器/容器镜像)。

7) 吊销(CRL & OCSP)

What

  • CRL:由 CA 发布的“被吊销证书列表”;
  • OCSP:在线查询某一证书是否有效;
  • OCSP Stapling:服务器把新鲜的 OCSP 响应随握手一并提供,减少客户端外呼。

Why

  • 私钥泄露、误签、主体不再合法等场景必须“立即失效”证书。

When

  • 任何你怀疑密钥泄露、错误签发、主体终止时;
  • 公网站点建议开启 Stapling 以降低时延与隐私泄露。

Who

  • 吊销方:签发该证书的 CA;
  • 查询方:客户端/浏览器/网关。

How(实践)

  • 部署 OCSP Stapling(Nginx/Envoy/Apache 支持);
  • 监控 CRL/OCSP 可用性;
  • 对关键内网系统引入 短周期证书 + 自动轮换,降低对吊销实时性的依赖。

好的,我们继续把证书体系中的 EKU、CRL、OCSP 拆开讲清楚,从 What / Why / When / Who / How 五个角度来理解。


8) EKU (Extended Key Usage,扩展密钥用法)

What

  • EKU 是证书中的一个扩展字段(X.509 v3 扩展),用来指定该证书的具体用途。
  • 例如:

    • serverAuth → 用于服务器身份认证(HTTPS 网站证书)
    • clientAuth → 用于客户端身份认证(双向 TLS)
    • codeSigning → 用于代码签名(软件发布)
    • emailProtection → 用于加密/签名电子邮件
    • timeStamping → 用于时间戳服务

Why

  • 防止证书被误用。比如一个服务器证书不能用来签发代码,否则可能被滥用。
  • 明确证书的边界,提高安全性和合规性。

When

  • 当颁发证书时,CA 会根据用途设置 EKU。
  • 浏览器或系统在使用证书时会验证 EKU 字段,确保用途匹配。

Who

  • 由 CA 签发时设定。
  • 客户端(浏览器、操作系统、应用程序)负责验证。

How

  • 证书中 Extended Key Usage OID 表示用途。
  • 例如:

    • 1.3.6.1.5.5.7.3.1 → TLS Web Server Authentication
    • 1.3.6.1.5.5.7.3.2 → TLS Web Client Authentication

9) CRL (Certificate Revocation List,证书吊销列表)

What

  • CRL 是由 CA 定期发布的“被吊销的证书列表”。
  • 包含证书序列号、吊销时间、吊销原因。

Why

  • 有些证书在有效期内可能需要失效:

    • 私钥泄露
    • 持有者身份不再可信
    • 证书错误签发
  • CRL 能让客户端避免信任已不再安全的证书。

When

  • CA 会定期(如每天或每周)生成并发布 CRL 文件。
  • 客户端在验证证书时,可以下载并检查证书是否在 CRL 中。

Who

  • 由 CA 维护和发布。
  • 客户端(浏览器、系统)使用。

How

  • CA 在证书中写入 CRL Distribution Point (CDP) 扩展,告诉客户端从哪里获取 CRL。
  • 客户端下载 CRL 文件(一般是 .crl 格式),检查证书序列号是否在其中。

⚠️ 缺点:CRL 文件可能很大,更新不够实时。


10) OCSP (Online Certificate Status Protocol,在线证书状态协议)

What

  • OCSP 是一种比 CRL 更高效的证书状态检查协议。
  • 通过网络实时向 CA 或 OCSP 服务器查询某个证书是否有效。

Why

  • CRL 太笨重(下载整个列表),OCSP 可以直接查询单个证书的状态。
  • 更实时,通常延迟只有几秒到几分钟。

When

  • 浏览器访问 HTTPS 网站时,会发送 OCSP 请求,CA 响应“Good / Revoked / Unknown”。
  • 在安全性要求高的场景(金融、支付、政务系统)尤其常用。

Who

  • OCSP Responder:通常由颁发证书的 CA 运行。
  • 客户端:浏览器、应用、操作系统负责发起查询。

How

  • 证书里会有 Authority Information Access (AIA) 扩展,指明 OCSP 服务器的 URL。
  • 客户端构造一个 OCSP 请求,发送到服务器。
  • 服务器返回一个带签名的响应:

    • good → 证书有效
    • revoked → 证书已吊销
    • unknown → 不认识这个证书

⚠️ 缺点:

  • 如果 OCSP 服务器宕机,可能导致访问延迟或失败。
  • 有些浏览器会用 OCSP Stapling(服务器提前缓存并返回 OCSP 响应,避免客户端单独查询)。

2. 典型使用场景与决策

Web 服务器(单域/多域)

  • Why:HTTPS 与 SEO/合规;
  • When:所有公网服务默认启用;
  • How:ACME 自动签发与续期(Let’s Encrypt + certbot/cert-manager),SAN 列全域名;启用 HTTP/2/3,OCSP Stapling。

API 网关 ↔ 上游(内网)

  • Why:上游鉴别与加密;
  • How:网关验证上游服务证书链与 SAN;若需要双向认证,启用 mTLS 并为客户端颁发 ClientAuth 证书。

服务网格 / mTLS(微服务)

  • Why:零信任/细粒度身份;
  • How:Istio/Linkerd 会托管 CA(或对接外部 CA),为 Workload 自动签发短期证书(如 24h),Sidecar 负责轮换与分发;策略由 SPIFFE ID/SAN 绑定。

移动/IoT 设备

  • Why:抗中间人、设备身份;
  • How:出厂烧录设备私钥与证书,服务端校验;可结合证书**钉(pinning)**或透明日志(CT)。

3. 操作与安全最佳实践(给 Web/后端/DevOps)

  • 私钥绝不进代码库;尽量使用 KMS/HSM 或独立密钥卷。
  • 最小权限:密钥文件仅服务进程可读;容器用 readOnlyRootFilesystemfsGroup 控制访问。
  • 自动化:证书短期(30–90 天)+ 自动续期(ACME/cert-manager);失败告警。
  • 完整链部署:确保中级证书随同发送;用 openssl s_client -showcerts 或在线工具验证。
  • 严格 SAN:访问名必须出现在 SAN;别再依赖 CN。
  • 用途正确:KeyUsage/EKU 与角色匹配(ServerAuth/ClientAuth/CodeSigning)。
  • 日志与可观测:OCSP/CRL 查询、握手失败原因、证书过期预警。
  • 应急:密钥疑似泄露 → 立刻吊销 + 重新签发 + 强制轮换。

4. 快速命令备忘(OpenSSL 示例)

# 生成私钥(RSA 或 EC)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out server.key
# 或:EC P-256
openssl ecparam -genkey -name prime256v1 -noout -out server.key

# 生成含 SAN 的 CSR(用 -addext)
openssl req -new -key server.key -out server.csr \
  -subj "/CN=example.com/O=Example Ltd" \
  -addext "subjectAltName=DNS:example.com,DNS:www.example.com,DNS:api.example.com"

# 由中级 CA 签发(举例)
openssl x509 -req -in server.csr -CA intermediate.pem -CAkey intermediate.key \
  -CAcreateserial -out server.pem -days 398 -sha256 \
  -extfile <(printf "keyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\nsubjectAltName=DNS:example.com,DNS:www.example.com,DNS:api.example.com")

# 验证证书链与主机名(OpenSSL 3)
cat root.pem intermediate.pem > chain.pem
openssl verify -CAfile chain.pem -verify_hostname www.example.com server.pem

建议:把签发与存储迁到 ACME / cert-manager / step-ca / Vault PKI 等自动化方案;根 CA 离线保存,中级 CA 在线托管并审计。

5. Example

package main

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha1"
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/pem"
    "fmt"
    "math/big"
    "time"
)

// ---- Helpers ----

func mustRSA(bits int) *rsa.PrivateKey {
    key, err := rsa.GenerateKey(rand.Reader, bits)
    if err != nil {
        panic(err)
    }
    return key
}

func mustCreateCert(template, parent *x509.Certificate, pub *rsa.PublicKey, parentKey *rsa.PrivateKey) []byte {
    der, err := x509.CreateCertificate(rand.Reader, template, parent, pub, parentKey)
    if err != nil {
        panic(err)
    }
    return der
}

func pemEncodeCert(der []byte) []byte {
    return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
}

func pemEncodeKey(key *rsa.PrivateKey) []byte {
    return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
}

func mustSerial() *big.Int {
    serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
    if err != nil {
        panic(err)
    }
    return serial
}

func subjectKeyID(pub *rsa.PublicKey) []byte {
    spki, _ := x509.MarshalPKIXPublicKey(pub)
    sum := sha1.Sum(spki)
    return sum[:]
}

func printPEM(title string, b []byte) {
    fmt.Printf("\n===== %s =====\n%s\n", title, string(b))
}

func days(n int) time.Time { return time.Now().Add(time.Duration(n) * 24 * time.Hour) }

// ---- Main flow ----

func main() {
    // 1) 生成 Root CA(自签)
    rootKey := mustRSA(2048)
    rootTmpl := &x509.Certificate{
        SerialNumber:          mustSerial(),
        Subject:               pkix.Name{CommonName: "Demo Root CA", Organization: []string{"Acme Corp"}},
        NotBefore:             time.Now().Add(-time.Hour),
        NotAfter:              days(3650), // 10 年
        KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
        ExtKeyUsage:           []x509.ExtKeyUsage{},
        BasicConstraintsValid: true,
        IsCA:                  true,
        MaxPathLen:            2,
        SubjectKeyId:          subjectKeyID(&rootKey.PublicKey),
    }
    rootDER := mustCreateCert(rootTmpl, rootTmpl, &rootKey.PublicKey, rootKey)
    rootCert, _ := x509.ParseCertificate(rootDER)

    printPEM("ROOT CA CERT", pemEncodeCert(rootDER))
    printPEM("ROOT CA KEY", pemEncodeKey(rootKey))

    // 2) 生成 Intermediate CA,由 Root 签发
    intKey := mustRSA(2048)
    intTmpl := &x509.Certificate{
        SerialNumber:          mustSerial(),
        Subject:               pkix.Name{CommonName: "Demo Intermediate CA", Organization: []string{"Acme Corp"}},
        NotBefore:             time.Now().Add(-time.Hour),
        NotAfter:              days(3650),
        KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
        BasicConstraintsValid: true,
        IsCA:                  true,
        MaxPathLen:            1,
        SubjectKeyId:          subjectKeyID(&intKey.PublicKey),
        AuthorityKeyId:        rootCert.SubjectKeyId,
    }
    intDER := mustCreateCert(intTmpl, rootCert, &intKey.PublicKey, rootKey)
    intCert, _ := x509.ParseCertificate(intDER)

    printPEM("INTERMEDIATE CA CERT", pemEncodeCert(intDER))
    printPEM("INTERMEDIATE CA KEY", pemEncodeKey(intKey))

    // 3) 生成服务器私钥与 CSR(包含 SAN)
    leafKey := mustRSA(2048)
    csrTmpl := &x509.CertificateRequest{
        Subject:  pkix.Name{CommonName: "fanyamin.com", Organization: []string{"Fanyamin Ltd"}},
        DNSNames: []string{"fanyamin.com", "www.fanyamin.com", "api.fanyamin.com"}, // SAN
    }
    csrDER, err := x509.CreateCertificateRequest(rand.Reader, csrTmpl, leafKey)
    if err != nil {
        panic(err)
    }
    csr, err := x509.ParseCertificateRequest(csrDER)
    if err != nil {
        panic(err)
    }
    if err := csr.CheckSignature(); err != nil {
        panic(fmt.Errorf("CSR signature invalid: %w", err))
    }
    printPEM("SERVER CSR", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}))
    printPEM("SERVER KEY", pemEncodeKey(leafKey))

    // 4) 中级 CA 根据 CSR 签发服务器证书(服务器证书不是 CA)
    leafTmpl := &x509.Certificate{
        SerialNumber:          mustSerial(),
        Subject:               csr.Subject, // 沿用 CSR 主题
        NotBefore:             time.Now().Add(-time.Hour),
        NotAfter:              days(825), // 约 27 个月,符合常见限制
        KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
        ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
        BasicConstraintsValid: true,
        IsCA:                  false,
        DNSNames:              csr.DNSNames, // 从 CSR 注入 SAN
        SubjectKeyId:          subjectKeyID(&leafKey.PublicKey),
        AuthorityKeyId:        intCert.SubjectKeyId,
    }
    leafDER := mustCreateCert(leafTmpl, intCert, &leafKey.PublicKey, intKey)
    leafCert, _ := x509.ParseCertificate(leafDER)
    printPEM("SERVER CERT", pemEncodeCert(leafDER))

    // 5) 构建验证所需的根与中级池,并验证证书链 + 主机名
    roots := x509.NewCertPool()
    roots.AddCert(rootCert)

    inters := x509.NewCertPool()
    inters.AddCert(intCert)

    // 验证:主机名 www.fanyamin.com 是否被证书覆盖(SAN 中包含)
    if _, err := leafCert.Verify(x509.VerifyOptions{
        DNSName:       "www.fanyamin.com",
        Roots:         roots,
        Intermediates: inters,
    }); err != nil {
        panic(fmt.Errorf("verify failed (www.fanyamin.com): %w", err))
    }
    fmt.Println("✓ 证书链与主机名验证通过:www.fanyamin.com")

    // 故意用一个未在 SAN 中的主机名,看看失败情况
    if _, err := leafCert.Verify(x509.VerifyOptions{
        DNSName:       "not-in-san.fanyamin.com",
        Roots:         roots,
        Intermediates: inters,
    }); err != nil {
        fmt.Println("✗ 预期的验证失败(主机名不在 SAN):", err)
    } else {
        panic("unexpected: verification should have failed for not-in-san.fanyamin.com")
    }

    // 6) 补充:说明数字签名在链路验证中的体现
    fmt.Println("\n说明:以上 x509.Verify 实际做了“用上级证书公钥验证下级证书签名”的工作,")
    fmt.Println("即:Root 公钥 验证 Intermediate 的签名;Intermediate 公钥 验证 Leaf 的签名;")
    fmt.Println("再结合 DNSName 检查 SAN 与证书有效期、用途等扩展,最终确定是否可信。")
}

Comments |0|

Legend *) Required fields are marked
**) You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>
Category: 似水流年