寫在前面的話

CFSSL是CloudFlare旗下的PKI/TLS工具。可以用於數字簽名,簽名驗證和TLS證書捆綁的命令列工具和HTTP API伺服器。

是使用golang語言開發的證書工具。

官方地址:

github地址:https://github.com/cloudflare/cfssl

下載cfssl工具鏈

https://github.com/cloudflare/cfssl/releases

下載如下檔案

cfssl_1.6.0_darwin_amd64 表示cfssl的工具

cfssljson_1.6.0_darwin_amd64 表示使用json展示的工具

cfssl-certinfo_1.6.0_darwin_amd64 表示證書的檢視工具

軟連線生成cfssl和cfssljson

  1. ln -s ./cfssl_1.6.0_darwin_amd64 cfssl
    ln -s ./cfssl-certinfo_1.6.0_darwin_amd64 cfssl-certinfo
  2. ln -s ./cfssljson_1.6.0_darwin_amd64 cfssljson

  

使用cfssl生成證書步驟

1. 編寫CA根證書的證書籤名請求檔案

證書籤名請求(Certificate Signing Request)檔案,檔案格式為ca-csr.json,【檔名含義是CA of Certificate Signing Request】

ca-csr.json檔案中包含如下內容簡要說明

  • CN: Common Name,表示業務的名稱或者對外的域名。

  • C: Country, 表示國家

  • L: Locality,表示地區或城市

  • O: Organization Name,表示組織名稱或公司名稱

  • OU: Organizational Unit 表示組織單元名稱
  • ST: State,表示 州,省OU: Organization Unit Name,組織單位名稱或者部門

  • ca.expiry 表示證書的有效期,此處是20年
  • key.algo 表示證書的簽名演算法 使用rsa
  • hosts 表示要簽名的域名,此處是根證書,所以空著,用於簽名其他的證書。

ca-csr.json檔案內容如下:

  1. certs cat ca-csr.json|python -m json.tool
  2. {
  3. "CN": "voipman",
  4. "ca": {
  5. "expiry": "175200h"
  6. },
  7. "hosts": [],
  8. "key": {
  9. "algo": "rsa",
  10. "size": 2048
  11. },
  12. "names": [
  13. {
  14. "C": "CN",
  15. "L": "BeiJing",
  16. "O": "MyCompany",
  17. "OU": "MyTemp",
  18. "ST": "BeiJing"
  19. }
  20. ]
  21. }

2. 使用CA跟證書籤名請求檔案生成CA根證書。

  1. cfssl gencert -initca ca-csr.json|cfssljson -bare ca
  2. 2021/08/18 10:37:00 [INFO] generating a new CA key and certificate from CSR
  3. 2021/08/18 10:37:00 [INFO] generate received request
  4. 2021/08/18 10:37:00 [INFO] received CSR
  5. 2021/08/18 10:37:00 [INFO] generating key: rsa-2048
  6. 2021/08/18 10:37:00 [INFO] encoded CSR
  7. 2021/08/18 10:37:00 [INFO] signed certificate with serial number 116851465485290360665710914818380982850969052112

通過執行上面的命令,產生如下檔案:

ca.pem 表示CA根證書,可以公開

ca-key.pem 表示CA根證書的金鑰,不要公開

ca.csr 表示CA證書籤名請求

我們主要分析一下ca.pem證書檔案中的內容資訊

3. 檢視 ca.pem CA根證書

裡面包含什麼資訊呢,可以通過cfssl-certinfo工具檢視證書內容

  1. cfssl-certinfo -cert ca.pem
  2. {
  3. "subject": {
  4. "common_name": "voipman",
  5. "country": "CN",
  6. "organization": "MyCompany",
  7. "organizational_unit": "MyTemp",
  8. "locality": "BeiJing",
  9. "province": "BeiJing",
  10. "names": [
  11. "CN",
  12. "BeiJing",
  13. "BeiJing",
  14. "MyCompany",
  15. "MyTemp",
  16. "voipman"
  17. ]
  18. },
  19. "issuer": {
  20. "common_name": "voipman",
  21. "country": "CN",
  22. "organization": "MyCompany",
  23. "organizational_unit": "MyTemp",
  24. "locality": "BeiJing",
  25. "province": "BeiJing",
  26. "names": [
  27. "CN",
  28. "BeiJing",
  29. "BeiJing",
  30. "MyCompany",
  31. "MyTemp",
  32. "voipman"
  33. ]
  34. },
  35. "serial_number": "116851465485290360665710914818380982850969052112",
  36. "not_before": "2021-08-18T02:32:00Z",
  37. "not_after": "2041-08-13T02:32:00Z",
  38. "sigalg": "SHA256WithRSA",
  39. "authority_key_id": "",
  40. "subject_key_id": "34:9C:3B:8B:54:02:6F:2F:D3:F4:29:9B:23:23:6C:47:0D:0A:16:2B",
  41. "pem": "-----BEGIN CERTIFICATE-----\n.....\n-----END CERTIFICATE-----\n"
  42. }

從如上的ca.pem中可以看到

簽名演算法:"sigalg": "SHA256WithRSA"

證書生效時間:"not_before": "2021-08-18T02:32:00Z"

證書失效時間:"not_after": "2041-08-13T02:32:00Z",說明證書籤名請求檔案中設定的20年的證書有效期。

序列化:"serial_number": "116851465485290360665710914818380982850969052112",在前面生成證書時會打印出來。

證書內容:"pem": "-----BEGIN CERTIFICATE-----...,此處省調證書內容資訊。

其他欄位在證書籤名請求時已經做過介紹,此處忽略。

4. 編寫CA簽名配置檔案ca-config.json

  1. cat ca-config.json |python -m json.tool
  2. {
  3. "signing": {
  4. "default": {
  5. "expiry": "175200h"
  6. },
  7. "profiles": {
  8. "client": {
  9. "expiry": "175200h",
  10. "usages": [
  11. "signing",
  12. "key encipherment",
  13. "client auth"
  14. ]
  15. },
  16. "peer": {
  17. "expiry": "175200h",
  18. "usages": [
  19. "signing",
  20. "key encipherment",
  21. "client auth",
  22. "server auth"
  23. ]
  24. },
  25. "server": {
  26. "expiry": "175200h",
  27. "usages": [
  28. "signing",
  29. "key encipherment",
  30. "server auth"
  31. ]
  32. }
  33. }
  34. }
  35. }

欄位說明如下

  • signing, 表示ca.pem證書可用於簽名其它證書

  • profile中的peer配置的client auth 和 server auth
  • profile中的client配置的client auth
  • profile中的server配置的server auth
  • server auth:表示 客戶端client 可以用 CA證書 對 服務端server的證書進行簽名驗證。
  • client auth:表示 服務端server 可以用 CA證書 對 客戶端client 提供的證書進行簽名驗證。
  • server auth和client auth都存在時,說明客戶端和服務端雙向驗證。

如下業務域名證書生成選擇的profle是peer,表示雙向驗證。

同樣證書的失效日期是20年。

5. 編寫業務域名的證書籤名請求檔案 voipman-csr.json

  1. cat voipman-csr.json |python -m json.tool
  2. {
  3. "CN": "voipman",
  4. "hosts": [
  5. "127.0.0.1",
  6. "*.voipman.com",
  7. "localhost",
  8. "voipman.com",
  9. "*.vipman.com",
  10. "vipman.com"
  11. ],
  12. "key": {
  13. "algo": "rsa",
  14. "size": 2048
  15. },
  16. "names": [
  17. {
  18. "C": "CN",
  19. "L": "BeiJing",
  20. "O": "voipman",
  21. "OU": "MyTeam",
  22. "ST": "BeiJing"
  23. }
  24. ]
  25. }

這個業務域名簽名請求檔案的內容和ca-csr.json內容含義類似,關鍵部分是增加了hosts的配置,將需要簽名認證的ip地址和域名增加到hosts列表中。

6. 生成業務域名的證書和私鑰

  1. cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=peer voipman-csr.json|cfssljson -bare voipman-peer
  2. 2021/08/18 11:15:09 [INFO] generate received request
  3. 2021/08/18 11:15:09 [INFO] received CSR
  4. 2021/08/18 11:15:09 [INFO] generating key: rsa-2048
  5. 2021/08/18 11:15:09 [INFO] encoded CSR
  6. 2021/08/18 11:15:09 [INFO] signed certificate with serial number 178028283460672116734375677106692089761404461988

此命令的引數說明

生成證書命令:cfssh gencerts

使用ca證書:-ca=ca.pem

使用ca的金鑰:-ca-key=ca-key.pem

使用ca簽名證書的配置:-config=ca-config.json

選擇ca簽名證書配置的profile項:-profile=peer

選擇業務域名的證書籤名請求檔案:voipman-csr.json

生成業務域名的私鑰和證書檔案:cfssljson -bare voipman-peer 會生成voipman-peer.pem證書檔案和voipman-peer-key.pem的私鑰檔案。

執行如上命令後,產生如下檔案

voipman-peer.pem 業務域名的證書檔案,可以直接公開給請求端使用。
voipman-peer-key.pem 業務域名的私鑰,不可公開。
voipman-peer.csr 業務域名的證書籤名請求檔案。

檢視 業務域名的證書檔案voipman-peer.pem的內容

  1. cfssl-certinfo -cert voipman-peer.pem
  2. {
  3. "subject": {
  4. "common_name": "voipman",
  5. "country": "CN",
  6. "organization": "voipman",
  7. "organizational_unit": "MyTeam",
  8. "locality": "BeiJing",
  9. "province": "BeiJing",
  10. "names": [
  11. "CN",
  12. "BeiJing",
  13. "BeiJing",
  14. "voipman",
  15. "MyTeam",
  16. "voipman"
  17. ]
  18. },
  19. "issuer": {
  20. "common_name": "voipman",
  21. "country": "CN",
  22. "organization": "MyCompany",
  23. "organizational_unit": "MyTemp",
  24. "locality": "BeiJing",
  25. "province": "BeiJing",
  26. "names": [
  27. "CN",
  28. "BeiJing",
  29. "BeiJing",
  30. "MyCompany",
  31. "MyTemp",
  32. "voipman"
  33. ]
  34. },
  35. "serial_number": "178028283460672116734375677106692089761404461988",
  36. "sans": [
  37. "*.voipman.com",
  38. "localhost",
  39. "voipman.com",
  40. "*.vipman.com",
  41. "vipman.com",
  42. "127.0.0.1"
  43. ],
  44. "not_before": "2021-08-18T03:10:00Z",
  45. "not_after": "2041-08-13T03:10:00Z",
  46. "sigalg": "SHA256WithRSA",
  47. "authority_key_id": "34:9C:3B:8B:54:02:6F:2F:D3:F4:29:9B:23:23:6C:47:0D:0A:16:2B",
  48. "subject_key_id": "AD:54:74:A0:BF:67:E7:B7:18:50:20:0A:77:57:F7:16:D3:62:80:F6",
  49. "pem": "-----BEGIN CERTIFICATE-----\n......\n-----END CERTIFICATE-----\n"
  50. }

如上可以看到,證書檔案的內容中

sans表示證書的支援的域名列表

authority_key_id 表示是本證書是從哪個CA證書籤名生成的證書,業務域名正式的authority_key_id等於CA證書的subject_key_id。

其他欄位說明見簽名對CA證書的欄位說明。

使用如下命令查詢業務域名的證書籤名請求資訊

如下內容省略了部分內容,重點說明一下內容中的公鑰PublicKey(N模數,E公鑰指數)

  1. cfssl certinfo -csr voipman-peer.csr
  2. {
  3. "Raw": "xxx",
  4. "RawTBSCertificateRequest": "xxx,
  5. "RawSubject": "xxx",
  6. "Version": 0,
  7. "Signature": "xxx",
  8. "SignatureAlgorithm": 4,
  9. "PublicKeyAlgorithm": 1,
  10. "PublicKey": {
  11. "N": 252598034519828318357090360116071250272xxxxxxxxxxxxxx....xxxxx7,
  12. "E": 65537
  13. }
  14.  
  15. "Subject": {
  16.  
  17. },
  18. "DNSNames": [
  19. "*.voipman.com",
  20. "localhost",
  21. "voipman.com",
  22. "*.vipman.com",
  23. "vipman.com"
  24. ],
  25. "EmailAddresses": null,
  26. "IPAddresses": [
  27. "127.0.0.1"
  28. ],
  29. "URIs": null
  30. }

7. 使用golang的grpc驗證業務域名的證書和私鑰

編寫proto介面檔案,檔案命名為test.proto,定義一個EchoService介面,服務端實現時,將請求資料轉成大寫返回。

  1. syntax = "proto3";
  2. package test;
  3.  
  4. service EchoService {
  5. rpc Echo (Request) returns (Response) {}
  6. }
  7.  
  8. message Request {
  9. string data = 1;
  10. }
  11.  
  12. message Response {
  13. string data = 1;
  14. }

生成go的grpc程式碼

  1. mkdir -p ../src/test && protoc --go_out=plugins=grpc:../src/test/ ./test.proto

會生成 src/test/test.pb.go程式碼檔案。

編寫gRPC的服務端驗證程式碼,配置證書voipman-peer.pem和私鑰voipman-peer-key.pem

程式碼如下所示

  1. package main
  2. import (
  3. "cert-verify/src/test"
  4. "context"
  5. "fmt"
  6. "google.golang.org/grpc"
  7. "google.golang.org/grpc/credentials"
  8. "google.golang.org/grpc/reflection"
  9. "log"
  10. "net"
  11. "strings"
  12. )
  13. type EchoServer struct{
  14. }
  15.  
  16. func (s *EchoServer) Echo(ctx context.Context, in *test.Request) (*test.Response, error) {
  17. fmt.Println("RequestData: " + in.Data)
  18. return &test.Response{Data: strings.ToUpper(in.Data)}, nil
  19. }
  20.  
  21. func main() {
  22. listen, err := net.Listen("tcp", "0.0.0.0:9025")
  23. if err != nil {
  24. log.Fatalf("Failed to listen: %v", err)
  25. }
  26. creds, cerErr := credentials.NewServerTLSFromFile("./certs/voipman-peer.pem", "./certs/voipman-peer-key.pem")
  27. if cerErr != nil {
  28. log.Fatalf("Failed to load cert error: %v", cerErr)
  29. }
  30. var grpcServer *grpc.Server
  31. grpcServer = grpc.NewServer(grpc.Creds(creds))
  32. test.RegisterEchoServiceServer(grpcServer, &EchoServer{})
  33. reflection.Register(grpcServer)
  34. grpcServer.Serve(listen)
  35. }

編寫gRPC的客戶端程式碼,使用voipman-peer.pem證書請求如上的gRPC服務端

  1. package main
  2. import (
  3. "cert-verify/src/test"
  4. "golang.org/x/net/context"
  5. "google.golang.org/grpc"
  6. "google.golang.org/grpc/credentials"
  7. "log"
  8. )
  9.  
  10. func main() {
  11. hostNameList := []string {
  12. "dev.voipman.com",
  13. "test.voipman.com",
  14. "voipman.com",
  15. "127.0.0.1",
  16. "localhost",
  17. "dev.vipman.com",
  18. "test.vipman.com",
  19. "vipman.com",
  20. "dev.unknown.com",
  21. }
  22. for _, hostName := range hostNameList {
  23. url := "127.0.0.1:9025"
  24. creds, err := credentials.NewClientTLSFromFile("./certs/voipman-peer.pem", hostName)
  25. if err != nil {
  26. log.Printf("new rpc client tls fail %v", err)
  27. }
  28. clientConn, err := grpc.DialContext(context.Background(), url, grpc.WithTransportCredentials(creds))
  29. if err != nil {
  30. log.Printf("dail rpc server fail url:%v, err:%v", url, err)
  31. }
  32. if err != nil {
  33. log.Printf(err.Error())
  34. }
  35. defer clientConn.Close()
  36. cli := test.NewEchoServiceClient(clientConn)
  37. response, err := cli.Echo(context.Background(), &test.Request{Data: hostName})
  38. if err != nil {
  39. log.Fatalf("could not greet: %v", err)
  40. }
  41. log.Printf("HostName:%v, Response: %s", hostName, response.Data)
  42. }
  43. }

如上程式碼中,我們驗證voipman-csr.json中定義的hosts列表,如下測試域名應該可以通過證書的驗證。

  1. "dev.voipman.com",
    "test.voipman.com",
    "voipman.com",
    "127.0.0.1",
    "localhost",
    "dev.vipman.com",
    "test.vipman.com",
    "vipman.com",

    另外增加一條錯誤的域名地址,使用證書驗證應該不成功。

    執行結果如下
  1. 2021/08/18 13:38:12 HostName:dev.voipman.com, Response: DEV.VOIPMAN.COM
  2. 2021/08/18 13:38:12 HostName:test.voipman.com, Response: TEST.VOIPMAN.COM
  3. 2021/08/18 13:38:12 HostName:voipman.com, Response: VOIPMAN.COM
  4. 2021/08/18 13:38:12 HostName:127.0.0.1, Response: 127.0.0.1
  5. 2021/08/18 13:38:12 HostName:localhost, Response: LOCALHOST
  6. 2021/08/18 13:38:12 HostName:dev.vipman.com, Response: DEV.VIPMAN.COM
  7. 2021/08/18 13:38:12 HostName:test.vipman.com, Response: TEST.VIPMAN.COM
  8. 2021/08/18 13:38:12 HostName:vipman.com, Response: VIPMAN.COM
  9. 2021/08/18 13:38:12 could not greet: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate is valid for *.voipman.com, localhost, voipman.com, *.vipman.com, vipman.com, not dev.unknown.com"

從執行的結果來看,最後一條域名,使用證書和服務端建立認證時出現錯誤

could not greet: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate is valid for *.voipman.com, localhost, voipman.com, *.vipman.com, vipman.com, not dev.unknown.com"

這就說明了證書voipman.pem中的hosts含義所在。

祝玩的開心~

done.