基於公鑰驗籤實現應用許可機制

隨着互聯網的普及以及應用的快速發展,商業軟件的訂閱模式變得越來越流行。軟件公司開始提供基於訂閱的服務,用戶每月或每年支付費用以獲取軟件的使用權。這種模式使用戶可以更靈活地選擇服務期限,並且軟件公司可以持續提供更新和技術支持。隨着 “軟件定義汽車” 的到來,這種模式在智能網聯汽車領域也逐漸流行開來!

一些需要私有化部署在客戶現場的 toB 商業軟件的公司也在探索這種訂閱許可證模式,但與 toC 的軟件不同,toB 軟件系統由於部署在客戶數據中心中,如何有效地管理軟件授權成爲一個關鍵問題。傳統的通過註冊碼或者登錄供應商服務器進行軟件授權存在諸多不便 (甚至不可行),而利用公鑰基礎設施實現許可證簽發和驗證可以很好地解決這個問題。

本文就來探討一下如何利用公鑰證書驗籤的方式實現應用許可機制,在這套機制中,軟件供應商負責設計許可證格式對許可證進行簽名,並將證書分發給客戶。客戶只需要利用供應商提供的方法將證書導入系統或更新許可證即可,系統可自動識別許可證的有效性與並加載信息的變更。這種方式無需客戶每次連接服務器就可以離線驗證許可,既方便且安全,同時也可防止許可證被盜用或篡改。

  1. 方案原理

基於公鑰驗籤實現許可證驗證機制的原理並不複雜,如果你對非對稱加密有初步瞭解,你就能理解下圖中的方案工作流程:

從圖中可以看到,基於公鑰驗籤的許可證驗證利用了公鑰加密的不對稱結構讓簽發方 (軟件廠商) 和驗證方 (客戶) 擁有不對等的密鑰。

首先,許可證的簽發方 (軟件廠商) 需要爲某個客戶生成一對公鑰 (證書) 和私鑰,私鑰需要嚴格保密,公鑰 (證書) 可以公開,將伴隨軟件安裝包一併分發給客戶。

簽發方 (軟件廠商) 根據客戶購買的服務或產品信息生成許可證文件,其中包含客戶標識、授權信息等,然後使用其私鑰對該許可證文件內容進行數字簽名,形成帶簽名的許可證。

客戶收到許可證後,已安裝到客戶現場的應用會用公鑰對許可證的簽名進行驗證,驗證能夠證明該許可證確實來自該簽發方,且內容完整無篡改。許可證初次導入、續期或變更時,應用都會對許可證的簽名進行驗證。整個驗證過程是離線脫機的,無需連接簽發服務器。

驗證成功後,許可證生效。應用會使用許可證中攜帶的授權信息對應用的行爲進行控制與約束。

下面我們用一個 Go 實現的示例來演示一下這個方案。

  1. 許可證格式設計

我們先來爲示例程序設計一個許可證。

許可證的格式設計直接影響到許可證的生成、分發和驗證等流程的順利進行。許可證文件中需要包含能夠識別客戶與軟件信息的字段,如客戶名稱、客戶 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 文件的設計來製作一個許可證並簽發。

  1. 許可證的製作與簽發

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 編碼,最後存入許可證文件中。

這個許可證製作完畢後,就可以分發給客戶了。客戶拿到許可證,導入到系統中,這時系統就會對導入的許可證進行驗證。下面我們就接着來看看如何使用伴隨系統一起分發的公鑰證書對許可證進行驗籤。

  1. 許可證的驗證

對許可證驗證的過程和步驟可以用下面示意圖來表示:

我們看到:圖中 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")
 }
}
  1. 小結

本文介紹瞭如何利用數字簽名和公鑰基礎設施實現軟件許可證的安全可靠驗證。通過設計許可證格式,包含客戶標識、授權範圍等關鍵信息,並添加軟件供應商的數字簽名,可以生成包含授權信息和不可篡改性的許可證文件。許可證簽發方持有私鑰對證書內容進行簽名,而客戶側部署的系統則持有廠商的公鑰來驗證簽名的有效性。整個流程無需連接簽發服務器即可完成驗證。這種模式解決了傳統方式的諸多訪問控制難題,實現了可靠、安全、便捷的分佈式許可證驗證方式。

當然我們也需要注意一些該機制的潛在問題,如私鑰保護、公鑰可信傳遞等。同時當有人將系統和許可證做整體複製時,這個方案也無法限制住這種非授權使用 (只能等待許可證過期)。

最後,軟件廠商可以按產品、客戶來管理私鑰和簽發的證書 (如下圖所示):

本文示例所涉及的 Go 源碼可以在這裏下載。

注:代碼倉庫中的證書和 key 文件有效期爲一年,大家如發現證書已經過期,可以在 make_certs 目錄下重新生成各種證書和私鑰並 copy 到對應的其他目錄中去。


Gopher Daily(Gopher 每日新聞) - https://gopherdaily.tonybai.com

我的聯繫方式:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vUUYSPVWpBHcf6UQt3cLjg