用go語言開發(fā)一個REST Service例子阵具,實(shí)現(xiàn)服務(wù)器和客戶端雙向認(rèn)證
服務(wù)器端代碼如下
package main
import (
"fmt"
"log"
"flag"
"net/http"
"io/ioutil"
"crypto/tls"
"crypto/x509"
"encoding/json"
"github.com/gorilla/mux"
)
var (
port int
hostname string
caroots string
keyfile string
signcert string
)
func init() {
flag.IntVar(&port, "port", 8080, "The host port on which the REST server will listen")
flag.StringVar(&hostname, "hostname", "0.0.0.0", "The host name on which the REST server will listen")
flag.StringVar(&caroots, "caroot", "", "Path to file containing PEM-encoded trusted certificate(s) for clients")
flag.StringVar(&keyfile, "key", "", "Path to file containing PEM-encoded key file for service")
flag.StringVar(&signcert, "signcert", "", "Path to file containing PEM-encoded sign certificate for service")
}
func startServer(address string, caroots string, keyfile string, signcert string, router *mux.Router) {
pool := x509.NewCertPool()
caCrt, err := ioutil.ReadFile(caroots)
if err != nil {
log.Fatalln("ReadFile err:", err)
}
pool.AppendCertsFromPEM(caCrt)
s := &http.Server{
Addr: address,
Handler: router,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
ClientCAs: pool,
ClientAuth: tls.RequireAndVerifyClientCert,
},
}
err = s.ListenAndServeTLS(signcert, keyfile)
if err != nil {
log.Fatalln("ListenAndServeTLS err:", err)
}
}
func SayHello(w http.ResponseWriter, r *http.Request) {
log.Println("Entry SayHello")
res := map[string]string {"hello": "world"}
b, err := json.Marshal(res)
if err == nil {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
w.Write(b)
}
log.Println("Exit SayHello")
}
func main() {
flag.Parse()
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/service/hello", SayHello).Methods("GET")
var address = fmt.Sprintf("%s:%d", hostname, port)
fmt.Println("Server listen on", address)
startServer(address, caroots, keyfile, signcert, router)
fmt.Println("Exit main")
}
其中TLS配置項(xiàng)ClientAuth: tls.RequireAndVerifyClientCert表明需要對客戶端認(rèn)證遣蚀,也就是要完成服務(wù)器和客戶端的雙向認(rèn)證屁置。
生成服務(wù)端證書
生成服務(wù)端私鑰
$ openssl genrsa -out server.key 2048
或者
$ openssl genrsa -des3 -out server.key 2048
此時需要用戶輸入密碼,然后每次用到私鑰的時候都需要再次輸入密碼乎折。
注意這個私鑰非常重要沸枯,通常需要安全保存并且把讀寫權(quán)限改成600生成服務(wù)端證書請求文件
$ openssl req -new -key server.key -out server.csr -subj "/C=CN/ST=BJ/L=beijing/O=myorganization/OU=mygroup/CN=myname"
注意這一步生成的是證書請求文件淮蜈,不是證書文件,下面才會生成證書文件捐下。
生成客戶端端證書
這個過程和生成服務(wù)端證書一樣
- 生成客戶端私鑰
$ openssl genrsa -out client.key 2048
- 生成客戶證書請求文件
$ openssl req -new -key client.key -out client.csr -subj "/C=CN/ST=BJ/L=beijing/O=myorganization/OU=mygroup/CN=myname"
生成服務(wù)器和客戶端經(jīng)過簽名的證書
證書請求文件csr生成以后账锹,需要將其發(fā)送給CA認(rèn)證機(jī)構(gòu)進(jìn)行簽發(fā)以生成真正的證書文件,當(dāng)然在我們例子里坷襟,我們使用OpenSSL對該證書進(jìn)行自簽發(fā)奸柬。
生成根證書私鑰
$ openssl genrsa -out ca.key 2048
生成根證書請求文件
$ openssl req -new -key ca.key -out ca.csr -subj "/C=CN/ST=BJ/L=beijing/O=myorganization/OU=mygroup/CN=myname"
生成自簽名的根證書文件
$ openssl x509 -req -days 365 -sha1 -extensions v3_ca -signkey ca.key -in ca.csr -out ca.cer
利用已簽名根證書生成服務(wù)端證書和客戶端證書
** 生成服務(wù)端證書
$ openssl x509 -req -days 365 -sha1 -extensions v3_req -CA ca.cer -CAkey ca.key -CAcreateserial -in server.csr -out server.cer
** 生成客戶端證書
$ openssl x509 -req -days 365 -sha1 -extensions v3_req -CA ca.cer -CAkey ca.key -CAcreateserial -in client.csr -out client.cer
注意,關(guān)于extensions參數(shù)值v3_ca/v3_req的含義請參考o(jì)penssl.cnf配置文件
$ locate openssl.cnf
$ vim /etc/pki/tls/openssl.cnf
[ v3_req ]
basicConstraints = CA:FALSE
...
[ v3_ca ]
basicConstraints = CA:true
其中最重要的區(qū)別是婴程,標(biāo)識這是不是一個CA證書廓奕。
編譯運(yùn)行服務(wù)端程序
$ go build main.go
$ ./main -caroot ./ca.cer -key ./server.key -signcert ./server.cer
Server listen on 0.0.0.0:8080
運(yùn)行客戶端程序
$ curl --cacert ./ca.cer --key ./client.key --cert ./client.cer https://localhost:8080/service/hello
curl: (60) Peer's certificate has an invalid signature.
More details here: http://curl.haxx.se/docs/sslcerts.html
curl performs SSL certificate verification by default, using a "bundle"
of Certificate Authority (CA) public keys (CA certs). If the default
bundle file isn't adequate, you can specify an alternate file
using the --cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
the bundle, the certificate verification probably failed due to a
problem with the certificate (it might be expired, or the name might
not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
the -k (or --insecure) option.
很遺憾,你應(yīng)該看到上面的錯誤信息档叔,再查看服務(wù)端的日志:
2017/09/27 22:42:49 http: TLS handshake error from [::1]:56168: remote error: tls: bad certificate
提示證書無效桌粉,原因是我們的證書里Commone Name這個字段填的值是myname,而當(dāng)前服務(wù)器運(yùn)行的域名是localhost衙四,他們不匹配铃肯,Common Name是要授予證書的服務(wù)器域名或主機(jī)名。
我們修改修改服務(wù)器端證書传蹈,重新生成:
$ openssl req -new -key server.key -out server.csr -subj "/C=CN/ST=BJ/L=beijing/O=myorganization/OU=mygroup/CN=localhost"
$ openssl x509 -req -days 365 -sha1 -extensions v3_req -CA ca.cer -CAkey ca.key -CAcreateserial -in server.csr -out server.cer
再運(yùn)行押逼,看看是不是想要的結(jié)果:
(注意,根證書和客戶端證書不需要重新生成)
$ curl --cacert ./ca.cer --key ./client.key --cert ./client.cer https://localhost:8080/service/hello
{"hello":"world"}
這就是我們想要的結(jié)果惦界。
同理挑格,如果使用真實(shí)機(jī)器主機(jī)名或者域名,例如主機(jī)名saturn表锻,則
$ openssl req -new -key server.key -out server.csr -subj "/C=CN/ST=BJ/L=beijing/O=myorganization/OU=mygroup/CN=saturn"
$ openssl x509 -req -days 365 -sha1 -extensions v3_req -CA ca.cer -CAkey ca.key -CAcreateserial -in server.csr -out server.cer
$ curl --cacert ./ca.cer --key ./client.key --cert ./client.cer https://saturn:8080/service/hello
{"hello":"world"}
查看證書內(nèi)容
$ openssl x509 -in server.cer -text -noout 2>&1| head -n 15
Certificate:
Data:
Version: 1 (0x0)
Serial Number: 15163366668719918823 (0xd26f19a5700c8ee7)
Signature Algorithm: sha1WithRSAEncryption
Issuer: C=CN, ST=BJ, L=beijing, O=myorganization, OU=mygroup, CN=myname
Validity
Not Before: Sep 27 14:44:07 2017 GMT
Not After : Sep 27 14:44:07 2018 GMT
Subject: C=CN, ST=BJ, L=beijing, O=myorganization, OU=mygroup, CN=localhost
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:9e:f0:05:0f:1f:4d:43:36:65:86:36:5e:80:bb:
里面表示了當(dāng)前證書信息恕齐,以及簽發(fā)者的信息。
總結(jié)
每個節(jié)點(diǎn)(不管是客戶端還是服務(wù)端)都有一個證書文件和key文件瞬逊,他們用來互相加密解密显歧;因?yàn)樽C書里面包含public key,key文件里面包含private key确镊;他們構(gòu)成一對密鑰對士骤,是互為加解密的。
根證書是所有節(jié)點(diǎn)公用的蕾域,不管是客戶端還是服務(wù)端拷肌,都要先注冊根證書(通常這個過程是注冊到操作系統(tǒng)信任的根證書數(shù)據(jù)庫里面,在咱們這個例子里面沒有這么做旨巷,因?yàn)檫@是一個臨時的根證書巨缘,只在服務(wù)端和客戶端命令行中指定了一下),以示這個根證書是可信的采呐, 然后當(dāng)需要驗(yàn)證對方的證書時若锁,因?yàn)榇?yàn)證的證書是通過這個根證書簽名的,我們信任根證書斧吐,所以推導(dǎo)出也可以信任對方的證書又固。
所以如果需要實(shí)現(xiàn)雙向認(rèn)證仲器,那么每一端都需要三個文件
- {node}.cer: PEM certificate
己方證書文件,將會被發(fā)給對方仰冠,讓對方認(rèn)證 - {node}..key: PEM RSA private key
己方private key文件乏冀,用來解密經(jīng)己方證書(因?yàn)榘悍絧ublic key)加密的內(nèi)容,這個加密過程一般是由對方實(shí)施的洋只。 - ca.cer: PEM certificate
根證書文件辆沦,用來驗(yàn)證對方發(fā)過來的證書文件,所有由同一個根證書簽名的證書都應(yīng)該能驗(yàn)證通過木张。