2023年十月月 发布的文章

基于公钥验签实现应用许可机制

本文永久链接 – https://tonybai.com/2023/10/16/implementation-of-app-licensing-based-on-verifying-sign-by-pubkey

随着互联网的普及以及应用的快速发展,商业软件的订阅模式变得越来越流行。软件公司开始提供基于订阅的服务,用户每月或每年支付费用以获取软件的使用权。这种模式使用户可以更灵活地选择服务期限,并且软件公司可以持续提供更新和技术支持。随着“软件定义汽车”的到来,这种模式在智能网联汽车领域也逐渐流行开来!

一些需要私有化部署在客户现场的toB商业软件的公司也在探索这种订阅许可证模式,但与toC的软件不同,toB软件系统由于部署在客户数据中心中,如何有效地管理软件授权成为一个关键问题。传统的通过注册码或者登录供应商服务器进行软件授权存在诸多不便(甚至不可行),而利用公钥基础设施实现许可证签发和验证可以很好地解决这个问题。

本文就来探讨一下如何利用公钥证书验签的方式实现应用许可机制,在这套机制中,软件供应商负责设计许可证格式对许可证进行签名,并将证书分发给客户。客户只需要利用供应商提供的方法将证书导入系统或更新许可证即可,系统可自动识别许可证的有效性与并加载信息的变更。这种方式无需客户每次连接服务器就可以离线验证许可,既方便且安全,同时也可防止许可证被盗用或篡改。

1. 方案原理

基于公钥验签实现许可证验证机制的原理并不复杂,如果你对非对称加密有初步了解,你就能理解下图中的方案工作流程:

从图中可以看到,基于公钥验签的许可证验证利用了公钥加密的不对称结构让签发方(软件厂商)和验证方(客户)拥有不对等的密钥。

首先,许可证的签发方(软件厂商)需要为某个客户生成一对公钥(证书)和私钥,私钥需要严格保密,公钥(证书)可以公开,将伴随软件安装包一并分发给客户。

签发方(软件厂商)根据客户购买的服务或产品信息生成许可证文件,其中包含客户标识、授权信息等,然后使用其私钥对该许可证文件内容进行数字签名,形成带签名的许可证。

客户收到许可证后,已安装到客户现场的应用会用公钥对许可证的签名进行验证,验证能够证明该许可证确实来自该签发方,且内容完整无篡改。许可证初次导入、续期或变更时,应用都会对许可证的签名进行验证。整个验证过程是离线脱机的,无需连接签发服务器。

验证成功后,许可证生效。应用会使用许可证中携带的授权信息对应用的行为进行控制与约束。

下面我们用一个Go实现的示例来演示一下这个方案。

2. 许可证格式设计

我们先来为示例程序设计一个许可证。

许可证的格式设计直接影响到许可证的生成、分发和验证等流程的顺利进行。许可证文件中需要包含能够识别客户与软件信息的字段,如客户名称、客户ID、软件名称、版本号等,其中客户ID、版本号等信息要与内置于分发给客户的应用中的信息一致,在构建应用时可以通过类似下面的命令将客户ID、版本号等信息写入给客户定制的应用程序:

$go build -ldflags "-X main.version=$(version)" -o xxx

这些内容可以与许可证中的内容比对,防止许可证被不同客户滥用。

同时许可证还需要包含授权范围信息,如授权类型(试用版或正式版)、授权期限、业务相关的限制授权(比如:最大接入连接数量等)等,这决定了客户可以享受的软件使用权限。

以上的客户与软件信息和授权范围信息被称为许可证的有效载荷

最后,许可证还要包含签名信息,以防止许可证文件被非法修改。签名信息通常是的对有效载荷信息的摘要进行运算后的结果。有了签名信息后,许可证就算制作完成了,并可以分发给客户导入到系统中。

统一格式的许可证文件便于厂商生成,也便于客户侧系统的解析与验证。

下面是我们为示例设计的license文件(.lic)的例子:

{
  "license": {
    "id": "01234567890",
    "vendor": "XYZ Company",
    "issuedTo": "DDD Company",
    "issuedDate": "2023-10-01T00:00:00Z",
    "expirationDate": "2024-09-30T23:59:59Z",
    "product": "My App",
    "version": "1.0",
    "licenseType": "Enterprise",
    "maxConnections": 1000
  },
  "signature": {
    "algorithm": "SHA256withRSA",
    "value": "Cm73yXxA7g0JOWel9xIZtyYOqAcFUnrOectrnI3jX9iQC9NVt61CuZogFdm72uPO5o+h4NhFEy0Lymgt29XFWEEVqrUnZuNRZee5W3UXsPC5vkhVt414Co5rsXuFFV/2UDFt36sF7rp30H53H/M7TCUF0spEfx+ybilS4xC5AjCPC4/1G7swQ2zCVvBfvQXhZkz953DdgMD3sBsqU2i0mMPbMHGGH6J6wXoHjCC6VQ0e3azVTVhiA40kxo5/uI0+ENOo559NIiPaZiAkgZgiuRFybJFk5Ib705BuaNHw6HfRk5DnxmWF/852cv32hT7it0is77p0wGODACkNkPL7YQ=="
  }
}

接下来我们就基于这个license文件的设计来制作一个许可证并签发。

3. 许可证的制作与签发

3.1 生成客户专用的私钥和公钥证书

为了给客户制作许可证,我们需要为客户生成一对专用的私钥和公钥证书,这个过程与《Go TLS服务端绑定证书的几种方式》一文中的证书制作步骤一致,我们来看一下生成私钥和公钥证书的代码:

// app-licensing/make_certs/main.go

func main() {
    // 生成CA根证书密钥对
    caKey, err := rsa.GenerateKey(rand.Reader, 2048)
    checkError(err)

    // 生成CA证书模板
    caTemplate := x509.Certificate{
        SerialNumber: big.NewInt(1),
        Subject: pkix.Name{
            Organization: []string{"Go CA"},
        },
        NotBefore:             time.Now(),
        NotAfter:              time.Now().Add(time.Hour * 24 * 365),
        KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
        BasicConstraintsValid: true,
        IsCA:                  true,
    }

    // 使用模板自签名生成CA证书
    caCert, err := x509.CreateCertificate(rand.Reader, &caTemplate, &caTemplate, &caKey.PublicKey, caKey)
    checkError(err)

    // 生成中间CA密钥对
    interKey, err := rsa.GenerateKey(rand.Reader, 2048)
    checkError(err)

    // 生成中间CA证书模板
    interTemplate := x509.Certificate{
        SerialNumber: big.NewInt(2),
        Subject: pkix.Name{
            Organization: []string{"Go Intermediate CA"},
        },
        NotBefore:             time.Now(),
        NotAfter:              time.Now().Add(time.Hour * 24 * 365),
        KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
        BasicConstraintsValid: true,
        IsCA:                  true,
    }

    // 用CA证书签名生成中间CA证书
    interCert, err := x509.CreateCertificate(rand.Reader, &interTemplate, &caTemplate, &interKey.PublicKey, caKey)
    checkError(err)

    // 生成叶子证书密钥对
    leafKey, err := rsa.GenerateKey(rand.Reader, 2048)
    checkError(err)

    // 生成叶子证书模板,CN为DDD Company
    leafTemplate := x509.Certificate{
        SerialNumber: big.NewInt(3),
        Subject: pkix.Name{
            Organization: []string{"DDD Company"},
            CommonName:   "ddd.com",
        },
        NotBefore:    time.Now(),
        NotAfter:     time.Now().Add(time.Hour * 24 * 365),
        KeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
        ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
        SubjectKeyId: []byte{1, 2, 3, 4},
    }

    // 用中间CA证书签名生成叶子证书
    leafCert, err := x509.CreateCertificate(rand.Reader, &leafTemplate, &interTemplate, &leafKey.PublicKey, interKey)
    checkError(err)

    // 将证书和密钥编码为PEM格式
    caCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert})
    caKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(caKey)})

    interCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: interCert})
    interKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(interKey)})

    leafCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert})
    leafKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafKey)})

    // 将PEM写入文件
    writeDataToFile("ca-cert.pem", caCertPEM)
    writeDataToFile("ca-key.pem", caKeyPEM)

    writeDataToFile("inter-cert.pem", interCertPEM)
    writeDataToFile("inter-key.pem", interKeyPEM)

    writeDataToFile("ddd-cert.pem", leafCertPEM)
    writeDataToFile("ddd-key.pem", leafKeyPEM)
}

我们分别生成了CA根、中间CA以及用于DDD Company许可证签发的专用key(ddd-key.pem)和公钥证书(ddd-cert.pem),执行上述代码后,我们将在目录下看到如下文件:

// app-licensing/make_certs

$go run main.go
$ls
ca-cert.pem ddd-cert.pem    go.mod      inter-key.pem
ca-key.pem  ddd-key.pem inter-cert.pem  main.go

3.2 制作许可证文件

有了ddd-key.pem后,我们就可以来制作专供DDD Company的许可证了。我们建立make_lic目录,将ddd-key.pem拷贝到该目录下。

下面是用于生成许可证文件的main函数代码片段(忽略了一些错误处理):

// app-licensing/make_lic/main.go

// 1. 建立对应license和Signature的结构体类型
type License struct {
    ID             string `json:"id"`
    Vendor         string `json:"vendor"`
    IssuedTo       string `json:"issuedTo"`
    IssuedDate     string `json:"issuedDate"`
    ExpirationDate string `json:"expirationDate"`
    Product        string `json:"product"`
    Version        string `json:"version"`
    LicenseType    string `json:"licenseType"`
    MaxConnections int    `json:"maxConnections"`
}

type Signature struct {
    Algorithm string `json:"algorithm"`
    Value     string `json:"value"`
}

func main() {
    keyData, _ := os.ReadFile("ddd-key.pem") // 加载私钥

    block, _ := pem.Decode(keyData)
    if block == nil || block.Type != "RSA PRIVATE KEY" {
        log.Fatal("failed to decode PEM block containing private key")
    }

    priKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
    if err != nil {
        log.Fatal(err)
    }

    // 2. 填充license的各个字段的值
    var license License
    license.ID = "01234567890"
    license.Vendor = "XYZ Company"
    license.IssuedTo = "DDD Company"
    license.IssuedDate = "2023-10-01T00:00:00Z"
    license.ExpirationDate = "2024-09-30T23:59:59Z"
    license.Product = "My App"
    license.Version = "1.0"
    license.LicenseType = "Enterprise"
    license.MaxConnections = 1000

    // 3. 将各个字段连接后sha256摘要
    data := []string{
        license.ID,
        license.Vendor,
        license.IssuedTo,
        license.IssuedDate,
        license.ExpirationDate,
        license.Product,
        license.Version,
        license.LicenseType,
        strconv.Itoa(license.MaxConnections),
    }
    payload := strings.Join(data, "")
    hash := sha256.Sum256([]byte(payload))

    // 4. 用私钥对摘要签名
    signed, _ := rsa.SignPKCS1v15(rand.Reader, priKey, crypto.SHA256, hash[:])

    // 5. 对签名结果base64编码
    signedB64 := base64.StdEncoding.EncodeToString(signed)

    // 6. 生成signature对象
    signature := Signature{
        Algorithm: "SHA256withRSA",
        Value:     signedB64,
    }

    // 7. 序列化为json
    fullLicense := map[string]interface{}{
        "license":   license,
        "signature": signature,
    }
    jsonData, _ := json.MarshalIndent(fullLicense, "", "  ")

    // 8. 保存为.lic文件
    os.WriteFile("ddd-company.lic", jsonData, 0644)
}

我们看到main函数制作许可证文件的步骤有很多,这里用下面这幅示意图来直观的说明一下:

证书的输入是有效载荷,包括客户与软件信息(比如ID、Product)、授权信息(比如IssueTo、IssuedDate、ExpirationDate等)、业务授权相关信息(比如MaxConnections等)。

我们将这些输入信息按声明顺序做字符串排列,并对获得的最终字符串做Sha256的单向散列得到摘要信息(摘要长度固定,运算速度较快)。

摘要信息是私钥签名的操作对象。签名后的信息转换为base64编码,最后存入许可证文件中。

这个许可证制作完毕后,就可以分发给客户了。客户拿到许可证,导入到系统中,这时系统就会对导入的许可证进行验证。下面我们就接着来看看如何使用伴随系统一起分发的公钥证书对许可证进行验签。

4. 许可证的验证

对许可证验证的过程和步骤可以用下面示意图来表示:

我们看到:图中verify signature有三个输入:公钥、从许可证文件中读取的经过base64 decode后的签名值(signature value)和基于许可证中字段计算出的摘要值。使用公钥对signature value进行运算得到的摘要值与基于许可证中字段计算出的摘要值如果一致,则说明验签成功。

基于图中流程,我们给出该示例验签部分的代码实现:

// app-licensing/verify_lic/main.go

func main() {

    // 1. 加载公钥证书,提取公钥
    certData, _ := os.ReadFile("ddd-cert.pem")
    block, _ := pem.Decode(certData)
    cert, _ := x509.ParseCertificate(block.Bytes)
    pubKey := cert.PublicKey.(*rsa.PublicKey)

    // 2. 解析许可证文件
    licData, err := os.ReadFile("ddd-company.lic")
    if err != nil {
        panic(err)
    }

    var license License
    var signature Signature

    err = json.Unmarshal(licData, &struct{ License *License }{&license})
    if err != nil {
        panic(err)
    }
    err = json.Unmarshal(licData, &struct{ Signature *Signature }{&signature})
    if err != nil {
        panic(err)
    }

    // 3. 生成签名摘要
    data := []string{
        license.ID,
        license.Vendor,
        license.IssuedTo,
        license.IssuedDate,
        license.ExpirationDate,
        license.Product,
        license.Version,
        license.LicenseType,
        strconv.Itoa(license.MaxConnections),
    }
    payload := strings.Join(data, "")
    hash := sha256.Sum256([]byte(payload))

    // 4. 使用公钥验签
    signValue, _ := base64.StdEncoding.DecodeString(signature.Value)
    err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hash[:], signValue)
    if err != nil {
        fmt.Println("Invalid signature:", err)
    } else {
        fmt.Println("Signature verified")
    }
}

5. 小结

本文介绍了如何利用数字签名和公钥基础设施实现软件许可证的安全可靠验证。通过设计许可证格式,包含客户标识、授权范围等关键信息,并添加软件供应商的数字签名,可以生成包含授权信息和不可篡改性的许可证文件。许可证签发方持有私钥对证书内容进行签名,而客户侧部署的系统则持有厂商的公钥来验证签名的有效性。整个流程无需连接签发服务器即可完成验证。这种模式解决了传统方式的诸多访问控制难题,实现了可靠、安全、便捷的分布式许可证验证方式。

当然我们也需要注意一些该机制的潜在问题,如私钥保护、公钥可信传递等。同时当有人将系统和许可证做整体复制时,这个方案也无法限制住这种非授权使用(只能等待许可证过期)。

最后,软件厂商可以按产品、客户来管理私钥和签发的证书(如下图所示):

本文示例所涉及的Go源码可以在这里下载。

注:代码仓库中的证书和key文件有效期为一年,大家如发现证书已经过期,可以在make_certs目录下重新生成各种证书和私钥并copy到对应的其他目录中去。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go TLS服务端绑定证书的几种方式

本文永久链接 – https://tonybai.com/2023/10/13/multiple-ways-to-bind-certificates-on-go-tls-server-side

随着互联网的发展,网站提供的服务类型和规模不断扩大,同时也对Web服务的安全性提出了更高的要求。TLS(Transport Layer Security)已然成为Web服务最重要的安全基础设施之一。默认情况下,一个TLS服务器通常只绑定一个证书,但当服务复杂度增加时,单一证书已然难以满足需求。这时,服务端绑定多个TLS证书就成为一个非常实用的功能。

Go语言中的net/http包和tls包对TLS提供了强大的支持,在密码学和安全专家Filippo Valsorda的精心设计下,Go提供了多种TLS服务端绑定证书的方式,本文将详细探讨服务端绑定TLS证书的几种方式,包括绑定单个证书、多个证书、自定义证书绑定逻辑等。我会配合示例代码,了解每种方式的使用场景、实现原理和优缺点。

注:本文假设读者已熟悉基本的TLS使用方法,并具备Go语言编程经验。如果你不具备Go语言基础知识,可以将学习我撰写的极客时间专栏《Go语言第一课》作为你入门Go的起点。

1. 热身:制作证书

为了后续示例说明方便,我们先来把示例所需的私钥和证书都做出来,本文涉及的证书以及他们之间的签发关系如下图:

注:示例使用的自签名根证书。

从图中我们看到,我们证书分为三个层次,最左边是CA的根证书(root certificate,比如ca-cert.pem),之后是根CA签发的中间CA证书(intermediate certificate,比如inter-cert.pem),从安全和管理角度出发,真正签发服务器证书的都是这些中间CA;最右侧则是由中间CA签发的叶子证书(leaf certificate,比如leaf-server-cert.pem),也就是服务器配置的服务端证书(server certificate),我们为三个不同域名创建了不同的服务器证书。

在这里,我们制作上述证书没有使用类似openssl这样的工具,而是通过Go代码生成的,下面是生成上述证书的代码片段:

// tls-certs-binding/make_certs/main.go

func main() {
    // 生成CA根证书密钥对
    caKey, err := rsa.GenerateKey(rand.Reader, 2048)
    checkError(err)

    // 生成CA证书模板
    caTemplate := x509.Certificate{
        SerialNumber: big.NewInt(1),
        Subject: pkix.Name{
            Organization: []string{"Go CA"},
        },
        NotBefore:             time.Now(),
        NotAfter:              time.Now().Add(time.Hour * 24 * 365),
        KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
        BasicConstraintsValid: true,
        IsCA:                  true,
    }

    // 使用模板自签名生成CA证书
    caCert, err := x509.CreateCertificate(rand.Reader, &caTemplate, &caTemplate, &caKey.PublicKey, caKey)
    checkError(err)

    // 生成中间CA密钥对
    interKey, err := rsa.GenerateKey(rand.Reader, 2048)
    checkError(err)

    // 生成中间CA证书模板
    interTemplate := x509.Certificate{
        SerialNumber: big.NewInt(2),
        Subject: pkix.Name{
            Organization: []string{"Go Intermediate CA"},
        },
        NotBefore:             time.Now(),
        NotAfter:              time.Now().Add(time.Hour * 24 * 365),
        KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
        BasicConstraintsValid: true,
        IsCA:                  true,
    }

    // 用CA证书签名生成中间CA证书
    interCert, err := x509.CreateCertificate(rand.Reader, &interTemplate, &caTemplate, &interKey.PublicKey, caKey)
    checkError(err)

    // 生成叶子证书密钥对
    leafKey, err := rsa.GenerateKey(rand.Reader, 2048)
    checkError(err)

    // 生成叶子证书模板,CN为server.com
    leafTemplate := x509.Certificate{
        SerialNumber: big.NewInt(3),
        Subject: pkix.Name{
            Organization: []string{"Go Server"},
            CommonName:   "server.com",
        },
        NotBefore:    time.Now(),
        NotAfter:     time.Now().Add(time.Hour * 24 * 365),
        KeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
        ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
        IPAddresses:  []net.IP{net.ParseIP("127.0.0.1")},
        DNSNames:     []string{"server.com"},
        SubjectKeyId: []byte{1, 2, 3, 4},
    }

    // 用中间CA证书签名生成叶子证书
    leafCert, err := x509.CreateCertificate(rand.Reader, &leafTemplate, &interTemplate, &leafKey.PublicKey, interKey)
    checkError(err)

    // 生成server1.com叶子证书
    leafKey1, _ := rsa.GenerateKey(rand.Reader, 2048)

    leafTemplate1 := x509.Certificate{
        SerialNumber: big.NewInt(4),
        Subject: pkix.Name{
            CommonName: "server1.com",
        },
        NotBefore: time.Now(),
        NotAfter:  time.Now().Add(time.Hour * 24 * 365),

        KeyUsage:    x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
        ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
        DNSNames:    []string{"server1.com"},
    }

    leafCert1, _ := x509.CreateCertificate(rand.Reader, &leafTemplate1, &interTemplate, &leafKey1.PublicKey, interKey)

    // 生成server2.com叶子证书
    leafKey2, _ := rsa.GenerateKey(rand.Reader, 2048)

    leafTemplate2 := x509.Certificate{
        SerialNumber: big.NewInt(5),
        Subject: pkix.Name{
            CommonName: "server2.com",
        },
        NotBefore: time.Now(),
        NotAfter:  time.Now().Add(time.Hour * 24 * 365),

        KeyUsage:    x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
        ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
        DNSNames:    []string{"server2.com"},
    }

    leafCert2, _ := x509.CreateCertificate(rand.Reader, &leafTemplate2, &interTemplate, &leafKey2.PublicKey, interKey)

    // 将证书和密钥编码为PEM格式
    caCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert})
    caKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(caKey)})

    interCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: interCert})
    interKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(interKey)})

    leafCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert})
    leafKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafKey)})

    leafCertPEM1 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert1})
    leafKeyPEM1 := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafKey1)})

    leafCertPEM2 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert2})
    leafKeyPEM2 := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafKey2)})

    // 将PEM写入文件
    writeDataToFile("ca-cert.pem", caCertPEM)
    writeDataToFile("ca-key.pem", caKeyPEM)

    writeDataToFile("inter-cert.pem", interCertPEM)
    writeDataToFile("inter-key.pem", interKeyPEM)

    writeDataToFile("leaf-server-cert.pem", leafCertPEM)
    writeDataToFile("leaf-server-key.pem", leafKeyPEM)

    writeDataToFile("leaf-server1-cert.pem", leafCertPEM1)
    writeDataToFile("leaf-server1-key.pem", leafKeyPEM1)

    writeDataToFile("leaf-server2-cert.pem", leafCertPEM2)
    writeDataToFile("leaf-server2-key.pem", leafKeyPEM2)
}

运行这个程序后,当前目录下就会出现如下私钥文件(xx-key.pem)和证书文件(xx-cert.pem):

$ls *pem
ca-cert.pem     inter-cert.pem      leaf-server-cert.pem    leaf-server1-cert.pem   leaf-server2-cert.pem
ca-key.pem      inter-key.pem       leaf-server-key.pem leaf-server1-key.pem    leaf-server2-key.pem

制作完证书后,我们就来看看日常使用最多的绑定单一TLS证书的情况。

2. 绑定单一TLS证书

做过web应用的读者,想必对绑定单一TLS证书的实现方式并不陌生。服务端只需要加载一对服务端私钥与公钥证书即可对外提供基于TLS的安全网络服务,这里一个echo服务为例,我们来看下服务端的代码:

// tls-certs-binding/bind_single_cert/sever/main.go

// 服务端
func startServer(certFile, keyFile string) {
    // 读取证书和密钥
    cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    if err != nil {
        log.Fatal(err)
    }

    // 创建TLS配置
    config := &tls.Config{
        Certificates: []tls.Certificate{cert},
    }

    // 启动TLS服务器
    listener, err := tls.Listen("tcp", ":8443", config)
    if err != nil {
        log.Fatal(err)
    }
    defer listener.Close()

    log.Println("Server started")

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Println(err)
            continue
        }
        handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    defer conn.Close()
    // 处理连接...
    // 循环读取客户端的数据
    for {
        buf := make([]byte, 1024)
        n, err := conn.Read(buf)
        if err != nil {
            // 读取失败则退出
            return
        }

        // 回显数据给客户端
        s := string(buf[:n])
        fmt.Printf("recv data: %s\n", s)
        conn.Write(buf[:n])
    }
}

func main() {
    // 启动服务器
    startServer("leaf-server-cert.pem", "leaf-server-key.pem")
}

根据TLS的原理,客户端在与服务端的握手过程中,服务端会将服务端证书(leaf-server-cert.pem)发到客户端供后者验证,客户端使用服务器公钥证书校验服务器身份。这一过程的实质是客户端利用CA证书中的公钥或中间CA证书中的公钥对服务端证书中由CA私钥或中间CA私钥签名的数据进行验签

// tls-certs-binding/bind_single_cert/client/main.go

func main() {
    caCert, err := ioutil.ReadFile("inter-cert.pem")
    if err != nil {
        log.Fatal(err)
    }

    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)

    config := &tls.Config{
        RootCAs: caCertPool,
    }

    conn, err := tls.Dial("tcp", "server.com:8443", config)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    // 每秒发送信息
    ticker := time.NewTicker(time.Second)
    for range ticker.C {
        msg := "hello, tls"
        conn.Write([]byte(msg))

        // 读取回复
        buf := make([]byte, len(msg))
        conn.Read(buf)
        log.Println(string(buf))
    }

}

这里我们使用了签发了leaf-server-cert.pem证书的中间CA(inter-cert.pem)来验证服务端证书(leaf-server-cert.pem)的合法性,毫无疑问这是会成功的!

// server
$go run main.go
2023/10/05 22:49:17 Server started

// client

$go run main.go
2023/10/05 22:49:22 hello, tls
2023/10/05 22:49:23 hello, tls
... ...

注:运行上述代码之前,需修改/etc/hosts文件,添加server.com的IP为127.0.0.1。

不过要注意的是,在这里用CA根证书(ca-cert.pem)直接验证叶子证书(leaf-server-cert.pem)会失败,因为根证书不是叶子证书的直接签发者,必须通过验证证书链来建立根证书和叶子证书之间的信任链。

3. 证书链

实际生产中,服务器实体证书和根证书分别只有一张,但中间证书可以有多张,这些中间证书在客户端并不一定存在,这就可能导致客户端与服务端的连接无法建立。通过openssl命令也可以印证这一点:

// 在make_certs目录下

// CA根证书无法直接验证叶子证书
$openssl verify -CAfile ca-cert.pem leaf-server-cert.pem
leaf-server-cert.pem: O = Go Server, CN = server.com
error 20 at 0 depth lookup:unable to get local issuer certificate

// 证书链不完整,也无法验证
$openssl verify -CAfile inter-cert.pem leaf-server-cert.pem
leaf-server-cert.pem: O = Go Intermediate CA
error 2 at 1 depth lookup:unable to get issuer certificate

// 需要用完整证书链来验证
$openssl verify -CAfile ca-cert.pem -untrusted inter-cert.pem leaf-server-cert.pem
leaf-server-cert.pem: OK

为此在建连阶段,服务端不仅要将服务器实体证书发给客户端,还要发送完整的证书链(如下图所示)。

证书链的最顶端是CA根证书,它的签名值是自己签名的,验证签名的公钥就包含在根证书中,根证书的签发者(Issuer)与使用者(Subject)是相同的。除了根证书,每个证书的签发者(Issuer)是它的上一级证书的使用者(Subject)。以上图为例,下列关系是成立的:

- ca-cert.pem的Issuer == ca-cert.pem的Subject
- inter1-cert.pem的Issuer == ca-cert.pem的Subject
- inter2-cert.pem的Issuer == inter1-cert.pem的Subject
... ...
- interN-cert.pem的Issuer == interN-1-cert.pem的Subject
- leaf-server-cert.pem的Issuer == interN-cert.pem的Subject

每张证书包含的重要信息是签发者(Issuer)、数字签名算法、签名值、使用者(Subject)域名、使用者公钥。除了根证书,每个证书(比如inter2-cert.pem证书)被它的上一级证书(比如inter1-cert.pem证书)对应的私钥签名,签名值包含在证书中,上一级证书包含的公钥可以用来验证该证书中的签名值(inter2-cert.pem证书可以用来验证inter1-cert.pem证书中的签名值)。

那么如何在服务端返回证书链呢?如何在客户端接收并验证证书链呢?我们来看下面示例。在这个示例中,客户端仅部署了根证书(ca-cert.pem),而服务端需要将服务证书与签发服务证书的中间CA证书以证书链的形式返回给客户端。

我们先来看服务端:

// tls-certs-binding/bind_single_cert/server-with-certs-chain/main.go

// 服务端
func startServer(certFile, keyFile string) {
    // 读取证书和密钥
    cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    if err != nil {
        log.Fatal(err)
    }

    interCertBytes, err := os.ReadFile("inter-cert.pem")
    if err != nil {
        log.Fatal(err)
    }

    interCertblock, _ := pem.Decode(interCertBytes)

    // 将中间证书添加到证书链
    cert.Certificate = append(cert.Certificate, interCertblock.Bytes)

    // 创建TLS配置
    config := &tls.Config{
        Certificates: []tls.Certificate{cert},
    }

    // 启动TLS服务器
    listener, err := tls.Listen("tcp", ":8443", config)
    if err != nil {
        log.Fatal(err)
    }
    defer listener.Close()

    log.Println("Server started")

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Println(err)
            continue
        }
        handleConnection(conn)
    }
}

我们看到:服务端在加载完服务端证书后,又将中间CA证书inter-cert.pem attach到cert.Certificate,这样cert.Certificate中就构造出了一个证书链,而不单单是一个服务端证书了。

我们要注意证书链构造时的顺序,这里按照的是如下顺序构造证书链的:

- 服务端证书 (leaf certificate)
- 中间CA证书N
- 中间CA证书N-1
... ...
- 中间CA证书2
- 中间CA证书1

如果客户端没有根CA证书 (root certificate),在服务端构造证书链时,需要将根CA证书作为最后一个证书attach到证书链中。

下面则是客户端验证证书链的代码:

// tls-certs-binding/bind_single_cert/client-verify-certs-chain/main.go

func main() {
    // 加载ca-cert.pem
    caCertBytes, err := os.ReadFile("ca-cert.pem")
    if err != nil {
        log.Fatal(err)
    }

    caCertblock, _ := pem.Decode(caCertBytes)
    caCert, err := x509.ParseCertificate(caCertblock.Bytes)
    if err != nil {
        log.Fatal(err)
    }

    // 创建TLS配置
    config := &tls.Config{
        InsecureSkipVerify: true, // trigger to call VerifyPeerCertificate

        // 设置证书验证回调函数
        VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
            // 解析服务端返回的证书链(顺序:server-cert.pem, inter-cert.pem,inter-cert.pem's issuer...)

            var issuer *x509.Certificate
            var cert *x509.Certificate
            var err error

            if len(rawCerts) == 0 {
                return errors.New("no server certificate found")
            }

            issuer = caCert

            for i := len(rawCerts) - 1; i >= 0; i-- {
                cert, err = x509.ParseCertificate(rawCerts[i])
                if err != nil {
                    return err
                }

                if !verifyCert(issuer, cert) {
                    return errors.New("verifyCert failed")
                }

                issuer = cert
            }
            return nil
        },
    }

    conn, err := tls.Dial("tcp", "server.com:8443", config)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    // 每秒发送信息
    ticker := time.NewTicker(time.Second)
    for range ticker.C {
        msg := "hello, tls"
        conn.Write([]byte(msg))

        // 读取回复
        buf := make([]byte, len(msg))
        conn.Read(buf)
        log.Println(string(buf))
    }

}

// 验证cert是否是issuer的签发
func verifyCert(issuer, cert *x509.Certificate) bool {

    // 验证证书
    certPool := x509.NewCertPool()
    certPool.AddCert(issuer) // ok
    opts := x509.VerifyOptions{
        Roots: certPool,
    }
    _, err := cert.Verify(opts)
    return err == nil
}

从代码可以看到,当正常验证失败或没有使用正常验证的情况下,我们需要将InsecureSkipVerify设置为true才能触发证书链的自定义校验逻辑(VerifyPeerCertificate)。在VerifyPeerCertificate中,我们先用ca根证书校验位于证书链最后的那个证书,验证成功后,用验证成功的证书验证倒数第二个证书,依次类推,知道全部证书都校验ok,说明证书链是可信任的。

服务端绑定一个证书或一套证书链是最简单的,也是最常见的方案,但在一些场景下,比如考虑支持多个域名、证书轮换等,TLS服务端可能需要绑定多个证书以满足要求。下面我们就来看看如何为TLS服务端绑定多个证书。

4. 绑定多个TLS证书

这个示例的证书绑定情况如下图:

我们在服务端部署并绑定了三个证书,三个证书与域名的对应关系如下:

- 证书leaf-server-cert.pem 对应 server.com
- 证书leaf-server1-cert.pem 对应 server1.com
- 证书leaf-server2-cert.pem 对应 server2.com

注:在/etc/hosts中添加server1.com和server2.com对应的ip均为127.0.0.1。

// tls-certs-binding/bind_multi_certs/server/main.go

func main() {
    certFiles := []string{"leaf-server-cert.pem", "leaf-server1-cert.pem", "leaf-server2-cert.pem"}
    keyFiles := []string{"leaf-server-key.pem", "leaf-server1-key.pem", "leaf-server2-key.pem"}

    // 启动服务器
    startServer(certFiles, keyFiles)
}

// 服务端
func startServer(certFiles, keyFiles []string) {
    // 读取证书和密钥

    var certs []tls.Certificate
    for i := 0; i < len(certFiles); i++ {
        cert, err := tls.LoadX509KeyPair(certFiles[i], keyFiles[i])
        if err != nil {
            log.Fatal(err)
        }
        certs = append(certs, cert)
    }

    // 创建TLS配置
    config := &tls.Config{
        Certificates: certs,
    }

    // 启动TLS服务器
    listener, err := tls.Listen("tcp", ":8443", config)
    if err != nil {
        log.Fatal(err)
    }
    defer listener.Close()

    log.Println("Server started")

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Println(err)
            continue
        }
        handleConnection(conn)
    }
}

我们看到,绑定多个证书与绑定一个证书的原理是完全一样的,tls.Config的Certificates字段原本就是一个切片,可以容纳单个证书,也可以容纳证书链,容纳多个证书也不是问题。

客户端代码变化不大,我们仅是通过下面代码输出了服务端返回的证书的Subject.CN:

// tls-certs-binding/bind_multi_certs/client/main.go

// 解析连接的服务器证书
certs := conn.ConnectionState().PeerCertificates
if len(certs) > 0 {
    log.Println("Server CN:", certs[0].Subject.CommonName)
}

接下来我们通过client连接不同的域名,得到如下执行结果:

// 服务端
$go run main.go
2023/10/06 10:22:38 Server started

// 客户端
$go run main.go -server server.com:8443
2023/10/06 10:22:57 Server CN: server.com
2023/10/06 10:22:58 hello, tls

$go run main.go -server server1.com:8443
2023/10/06 10:23:02 Server CN: server1.com
2023/10/06 10:23:03 hello, tls
2023/10/06 10:23:04 hello, tls

$go run main.go -server server2.com:8443
2023/10/06 10:23:08 Server CN: server2.com
2023/10/06 10:23:09 hello, tls
... ...

我们看到,由于绑定多个域名对应的证书,程序可以支持访问不同域名的请求,并根据请求的域名,返回对应域名的证书。

5. 自定义证书选择绑定逻辑

无论是单一TLS证书、证书链还是多TLS证书,他们都有一个共同特点,那就是证书的绑定是事先已知的,是一种“静态”模式的绑定;有些场景下,服务端在初始化启动后并不会绑定某个固定的证书,而是根据客户端的连接需求以及特定规则在证书池中选择某个匹配的证书。在这种情况下,我们需要使用GetCertificate回调从自定义的证书池中选择匹配的证书,而不能在用上面示例中那种“静态”模式了。

我们来看一个自定义证书选择逻辑的示例,下面示意图展示了客户端和服务端的证书部署情况:

我们主要看一下服务端的代码逻辑变动:

// tls-certs-binding/bind_custom_logic/server/main.go

func startServer(certsPath string) {

    // 创建TLS配置
    config := &tls.Config{
        GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
            // 根据clientHello信息选择cert

            certFile := fmt.Sprintf("%s/leaf-%s-cert.pem", certsPath, info.ServerName[:len(info.ServerName)-4])
            keyFile := fmt.Sprintf("%s/leaf-%s-key.pem", certsPath, info.ServerName[:len(info.ServerName)-4])

            // 读取证书和密钥
            cert, err := tls.LoadX509KeyPair(certFile, keyFile)
            return &cert, err
        },
    }
    ... ...
}

我们看到: tls.Config我们建立了一个匿名函数赋值给了GetCertificate字段,该函数的实现逻辑就是根据客户端clientHello信息(tls握手时发送的信息)按照规则从证书池目录中查找并加载对应的证书与其私钥信息。示例使用ServerName来查找带有同名信息的证书。

例子的运行结果与上面的示例都差不多,这里就不赘述了。

利用这种动态的证书选择逻辑,我们还可以实现通过执行外部命令来获取证书、从数据库加载证书等。

6. 小结

通过本文的介绍,我们全面了解了在Go服务端绑定单个、多个TLS证书的各种方式。我们首先介绍了生成自签名证书的方法,这为我们的示例程序奠定了基础。然后我们详细探讨了绑定单证书、证书链、多证书、定制从证书池取特定证书的逻辑等不同机制的用法、优劣势和适用场景。同时,在介绍每种用法时,我们都用代码示例进一步解释了这些绑定方式的具体实现流程。

单证书TLS简单易理解,运行性能优异。多证书TLS在提高性能、安全性、便利管理等方面有着重要意义。而自定义证书选取逻辑则更加灵活。通过综合运用各种绑定机制,可以使我们的Go语言服务器端更加强大和灵活。

本文示例所涉及的Go源码可以在这里下载。

注:代码仓库中的证书和key文件有效期为一年,大家如发现证书已经过期,可以在make_certs目录下重新生成各种证书和私钥并copy到对应的其他目录中去。

7. 参考资料


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats