go语言实现双向TLS认证的REST Service

用go语言开发一个REST Service例子,实现服务器和客户端双向认证

服务器端代码如下

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配置项ClientAuth: tls.RequireAndVerifyClientCert表明需要对客户端认证,也就是要完成服务器和客户端的双向认证。

生成服务端证书

  • 生成服务端私钥
    $ openssl genrsa -out server.key 2048
    或者
    $ openssl genrsa -des3 -out server.key 2048
    此时需要用户输入密码,然后每次用到私钥的时候都需要再次输入密码。
    注意这个私钥非常重要,通常需要安全保存并且把读写权限改成600

  • 生成服务端证书请求文件
    $ openssl req -new -key server.key -out server.csr -subj "/C=CN/ST=BJ/L=beijing/O=myorganization/OU=mygroup/CN=myname"

注意这一步生成的是证书请求文件,不是证书文件,下面才会生成证书文件。

生成客户端端证书

这个过程和生成服务端证书一样

  • 生成客户端私钥
    $ 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"

生成服务器和客户端经过签名的证书

证书请求文件csr生成以后,需要将其发送给CA认证机构进行签发以生成真正的证书文件,当然在我们例子里,我们使用OpenSSL对该证书进行自签发。

  • 生成根证书私钥
    $ 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

  • 利用已签名根证书生成服务端证书和客户端证书
    ** 生成服务端证书
    $ 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

注意,关于extensions参数值v3_ca/v3_req的含义请参考openssl.cnf配置文件

$ locate openssl.cnf
$ vim /etc/pki/tls/openssl.cnf

[ v3_req ]
    basicConstraints = CA:FALSE
...
[ v3_ca ]
    basicConstraints = CA:true

其中最重要的区别是,标识这是不是一个CA证书。

编译运行服务端程序

$ go build main.go
$ ./main -caroot ./ca.cer -key ./server.key -signcert ./server.cer
Server listen on 0.0.0.0:8080

运行客户端程序

$ 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.

很遗憾,你应该看到上面的错误信息,再查看服务端的日志:
2017/09/27 22:42:49 http: TLS handshake error from [::1]:56168: remote error: tls: bad certificate

提示证书无效,原因是我们的证书里Commone Name这个字段填的值是myname,而当前服务器运行的域名是localhost,他们不匹配,Common Name是要授予证书的服务器域名或主机名。

我们修改修改服务器端证书,重新生成:

$ 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

再运行,看看是不是想要的结果:
(注意,根证书和客户端证书不需要重新生成)

$ curl --cacert ./ca.cer --key ./client.key --cert ./client.cer https://localhost:8080/service/hello
{"hello":"world"}

这就是我们想要的结果。
同理,如果使用真实机器主机名或者域名,例如主机名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"}

查看证书内容

$ 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:

里面表示了当前证书信息,以及签发者的信息。

总结

每个节点(不管是客户端还是服务端)都有一个证书文件和key文件,他们用来互相加密解密;因为证书里面包含public key,key文件里面包含private key;他们构成一对密钥对,是互为加解密的。

根证书是所有节点公用的,不管是客户端还是服务端,都要先注册根证书(通常这个过程是注册到操作系统信任的根证书数据库里面,在咱们这个例子里面没有这么做,因为这是一个临时的根证书,只在服务端和客户端命令行中指定了一下),以示这个根证书是可信的, 然后当需要验证对方的证书时,因为待验证的证书是通过这个根证书签名的,我们信任根证书,所以推导出也可以信任对方的证书。

所以如果需要实现双向认证,那么每一端都需要三个文件

  • {node}.cer: PEM certificate
    己方证书文件,将会被发给对方,让对方认证
  • {node}..key: PEM RSA private key
    己方private key文件,用来解密经己方证书(因为包含己方public key)加密的内容,这个加密过程一般是由对方实施的。
  • ca.cer: PEM certificate
    根证书文件,用来验证对方发过来的证书文件,所有由同一个根证书签名的证书都应该能验证通过。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容