go語言實(shí)現(xiàn)雙向TLS認(rèn)證的REST Service

用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)證通過木张。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末众辨,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子舷礼,更是在濱河造成了極大的恐慌鹃彻,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妻献,死亡現(xiàn)場離奇詭異蛛株,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)育拨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門谨履,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人熬丧,你說我怎么就攤上這事笋粟。” “怎么了析蝴?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵害捕,是天一觀的道長。 經(jīng)常有香客問我闷畸,道長尝盼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任佑菩,我火速辦了婚禮盾沫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘殿漠。我一直安慰自己赴精,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布绞幌。 她就那樣靜靜地躺著蕾哟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上渐苏,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機(jī)與錄音菇夸,去河邊找鬼琼富。 笑死,一個胖子當(dāng)著我的面吹牛庄新,可吹牛的內(nèi)容都是我干的鞠眉。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼择诈,長吁一口氣:“原來是場噩夢啊……” “哼械蹋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起羞芍,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤哗戈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后荷科,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體唯咬,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年畏浆,在試婚紗的時候發(fā)現(xiàn)自己被綠了胆胰。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡刻获,死狀恐怖蜀涨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蝎毡,我是刑警寧澤厚柳,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站顶掉,受9級特大地震影響草娜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜痒筒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一宰闰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧簿透,春花似錦移袍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至啡浊,卻和暖如春觅够,著一層夾襖步出監(jiān)牢的瞬間胶背,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工喘先, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钳吟,地道東北人。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓窘拯,卻偏偏與公主長得像红且,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子涤姊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容