寫在前面的話
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
ln -s ./cfssl_1.6.0_darwin_amd64 cfssl ln -s ./cfssl-certinfo_1.6.0_darwin_amd64 cfssl-certinfo 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 表示證照的簽名演算法,目前cfssl支援的簽名演算法只有rsa和ecdsa兩種。
- hosts 表示要簽名的域名,此處是根證照,所以空著,用於簽名其他的證照。
ca-csr.json檔案內容如下:
➜ certs cat ca-csr.json|python -m json.tool { "CN": "voipman", "ca": { "expiry": "175200h" }, "hosts": [], "key": { "algo": "rsa", "size": 2048 }, "names": [ { "C": "CN", "L": "BeiJing", "O": "MyCompany", "OU": "MyTemp", "ST": "BeiJing" } ] }
如果key的簽名演算法選擇rsa時,size可以取值2048,3072和4096這三個值
如果可用的簽名演算法選擇ecdsa時,將如上內容的key改成如下內容,size可以取值256,384和521三個值
"key":{ "algo":"ecdsa", "size":256 }
2. 使用CA跟證照籤名請求檔案生成CA根證照。
➜ cfssl gencert -initca ca-csr.json|cfssljson -bare ca 2021/08/18 10:37:00 [INFO] generating a new CA key and certificate from CSR 2021/08/18 10:37:00 [INFO] generate received request 2021/08/18 10:37:00 [INFO] received CSR 2021/08/18 10:37:00 [INFO] generating key: rsa-2048 2021/08/18 10:37:00 [INFO] encoded CSR 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工具檢視證照內容
cfssl-certinfo -cert ca.pem { "subject": { "common_name": "voipman", "country": "CN", "organization": "MyCompany", "organizational_unit": "MyTemp", "locality": "BeiJing", "province": "BeiJing", "names": [ "CN", "BeiJing", "BeiJing", "MyCompany", "MyTemp", "voipman" ] }, "issuer": { "common_name": "voipman", "country": "CN", "organization": "MyCompany", "organizational_unit": "MyTemp", "locality": "BeiJing", "province": "BeiJing", "names": [ "CN", "BeiJing", "BeiJing", "MyCompany", "MyTemp", "voipman" ] }, "serial_number": "116851465485290360665710914818380982850969052112", "not_before": "2021-08-18T02:32:00Z", "not_after": "2041-08-13T02:32:00Z", "sigalg": "SHA256WithRSA", "authority_key_id": "", "subject_key_id": "34:9C:3B:8B:54:02:6F:2F:D3:F4:29:9B:23:23:6C:47:0D:0A:16:2B", "pem": "-----BEGIN CERTIFICATE-----\n.....\n-----END CERTIFICATE-----\n" }
從如上的ca.pem中可以看到
簽名演算法:"sigalg": "SHA256WithRSA"
如果選擇ecdsa簽名演算法,簽名演算法就是: "sigalg": "ECDSAWithSHA256"
證照生效時間:"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
cat ca-config.json |python -m json.tool { "signing": { "default": { "expiry": "175200h" }, "profiles": { "client": { "expiry": "175200h", "usages": [ "signing", "key encipherment", "client auth" ] }, "peer": { "expiry": "175200h", "usages": [ "signing", "key encipherment", "client auth", "server auth" ] }, "server": { "expiry": "175200h", "usages": [ "signing", "key encipherment", "server auth" ] } } } }
欄位說明如下
-
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
cat voipman-csr.json |python -m json.tool { "CN": "voipman", "hosts": [ "127.0.0.1", "*.voipman.com", "localhost", "voipman.com", "*.vipman.com", "vipman.com" ], "key": { "algo": "rsa", "size": 2048 }, "names": [ { "C": "CN", "L": "BeiJing", "O": "voipman", "OU": "MyTeam", "ST": "BeiJing" } ] }
這個業務域名簽名請求檔案的內容和ca-csr.json內容含義類似,關鍵部分是增加了hosts的配置,將需要簽名認證的ip地址和域名增加到hosts列表中。
6. 生成業務域名的證照和私鑰
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=peer voipman-csr.json|cfssljson -bare voipman-peer 2021/08/18 11:15:09 [INFO] generate received request 2021/08/18 11:15:09 [INFO] received CSR 2021/08/18 11:15:09 [INFO] generating key: rsa-2048 2021/08/18 11:15:09 [INFO] encoded CSR 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的內容
cfssl-certinfo -cert voipman-peer.pem { "subject": { "common_name": "voipman", "country": "CN", "organization": "voipman", "organizational_unit": "MyTeam", "locality": "BeiJing", "province": "BeiJing", "names": [ "CN", "BeiJing", "BeiJing", "voipman", "MyTeam", "voipman" ] }, "issuer": { "common_name": "voipman", "country": "CN", "organization": "MyCompany", "organizational_unit": "MyTemp", "locality": "BeiJing", "province": "BeiJing", "names": [ "CN", "BeiJing", "BeiJing", "MyCompany", "MyTemp", "voipman" ] }, "serial_number": "178028283460672116734375677106692089761404461988", "sans": [ "*.voipman.com", "localhost", "voipman.com", "*.vipman.com", "vipman.com", "127.0.0.1" ], "not_before": "2021-08-18T03:10:00Z", "not_after": "2041-08-13T03:10:00Z", "sigalg": "SHA256WithRSA", "authority_key_id": "34:9C:3B:8B:54:02:6F:2F:D3:F4:29:9B:23:23:6C:47:0D:0A:16:2B", "subject_key_id": "AD:54:74:A0:BF:67:E7:B7:18:50:20:0A:77:57:F7:16:D3:62:80:F6", "pem": "-----BEGIN CERTIFICATE-----\n......\n-----END CERTIFICATE-----\n" }
如上可以看到,證照檔案的內容中
sans表示證照的支援的域名列表
authority_key_id 表示是本證照是從哪個CA證照籤名生成的證照,業務域名正式的authority_key_id等於CA證照的subject_key_id。
其他欄位說明見簽名對CA證照的欄位說明。
使用如下命令查詢業務域名的證照籤名請求資訊
如下內容省略了部分內容,重點說明一下內容中的公鑰PublicKey(N模數,E公鑰指數)
cfssl certinfo -csr voipman-peer.csr { "Raw": "xxx", "RawTBSCertificateRequest": "xxx, "RawSubject": "xxx", "Version": 0, "Signature": "xxx", "SignatureAlgorithm": 4, "PublicKeyAlgorithm": 1, "PublicKey": { "N": 252598034519828318357090360116071250272xxxxxxxxxxxxxx....xxxxx7, "E": 65537 } "Subject": { }, "DNSNames": [ "*.voipman.com", "localhost", "voipman.com", "*.vipman.com", "vipman.com" ], "EmailAddresses": null, "IPAddresses": [ "127.0.0.1" ], "URIs": null }
如果選擇ecdsa簽名演算法時,公鑰PublicKey資訊為如下形式
"PublicKey": { "Curve": { "P": 11579xxxxx1, "N": 11579xxxxxx8, "B": 410583xxxx1, "Gx": 484395xxx6, "Gy": 36134xxxxx09, "BitSize": 256, "Name": "P-256" }, "X": 1069xx854, "Y": 1105xx971 }
P: 代表有限域Fp的那個質數
G: 橢圓曲線上的一個基點G = (Gx, Gy)
N:G在Fp中規定的序號
7. 使用golang的grpc驗證業務域名的證照和私鑰
編寫proto介面檔案,檔案命名為test.proto,定義一個EchoService介面,服務端實現時,將請求資料轉成大寫返回。
syntax = "proto3"; package test; service EchoService { rpc Echo (Request) returns (Response) {} } message Request { string data = 1; } message Response { string data = 1; }
生成go的grpc程式碼
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
程式碼如下所示
package main import ( "cert-verify/src/test" "context" "fmt" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/reflection" "log" "net" "strings" ) type EchoServer struct{ } func (s *EchoServer) Echo(ctx context.Context, in *test.Request) (*test.Response, error) { fmt.Println("RequestData: " + in.Data) return &test.Response{Data: strings.ToUpper(in.Data)}, nil } func main() { listen, err := net.Listen("tcp", "0.0.0.0:9025") if err != nil { log.Fatalf("Failed to listen: %v", err) } creds, cerErr := credentials.NewServerTLSFromFile("./certs/voipman-peer.pem", "./certs/voipman-peer-key.pem") if cerErr != nil { log.Fatalf("Failed to load cert error: %v", cerErr) } var grpcServer *grpc.Server grpcServer = grpc.NewServer(grpc.Creds(creds)) test.RegisterEchoServiceServer(grpcServer, &EchoServer{}) reflection.Register(grpcServer) grpcServer.Serve(listen) }
編寫gRPC的客戶端程式碼,使用voipman-peer.pem證照請求如上的gRPC服務端
package main import ( "cert-verify/src/test" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "log" ) func main() { hostNameList := []string { "dev.voipman.com", "test.voipman.com", "voipman.com", "127.0.0.1", "localhost", "dev.vipman.com", "test.vipman.com", "vipman.com", "dev.unknown.com", } for _, hostName := range hostNameList { url := "127.0.0.1:9025" creds, err := credentials.NewClientTLSFromFile("./certs/voipman-peer.pem", hostName) if err != nil { log.Printf("new rpc client tls fail %v", err) } clientConn, err := grpc.DialContext(context.Background(), url, grpc.WithTransportCredentials(creds)) if err != nil { log.Printf("dail rpc server fail url:%v, err:%v", url, err) } if err != nil { log.Printf(err.Error()) } defer clientConn.Close() cli := test.NewEchoServiceClient(clientConn) response, err := cli.Echo(context.Background(), &test.Request{Data: hostName}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("HostName:%v, Response: %s", hostName, response.Data) } }
如上程式碼中,我們驗證voipman-csr.json中定義的hosts列表,如下測試域名應該可以通過證照的驗證。
"dev.voipman.com",
"test.voipman.com",
"voipman.com",
"127.0.0.1",
"localhost",
"dev.vipman.com",
"test.vipman.com",
"vipman.com",
另外增加一條錯誤的域名地址,使用證照驗證應該不成功。
執行結果如下
2021/08/18 13:38:12 HostName:dev.voipman.com, Response: DEV.VOIPMAN.COM 2021/08/18 13:38:12 HostName:test.voipman.com, Response: TEST.VOIPMAN.COM 2021/08/18 13:38:12 HostName:voipman.com, Response: VOIPMAN.COM 2021/08/18 13:38:12 HostName:127.0.0.1, Response: 127.0.0.1 2021/08/18 13:38:12 HostName:localhost, Response: LOCALHOST 2021/08/18 13:38:12 HostName:dev.vipman.com, Response: DEV.VIPMAN.COM 2021/08/18 13:38:12 HostName:test.vipman.com, Response: TEST.VIPMAN.COM 2021/08/18 13:38:12 HostName:vipman.com, Response: VIPMAN.COM 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含義所在。
另外,在cfssl中hosts支援 ip地址,DNS域名,email地址和URI這四類。
如下:
"cloudflare.com" "www.cloudflare.com" "192.168.0.1" "jdoe@example.com" "https://www.cloudflare.com"
祝玩的開心~
done.