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
    根证书文件,用来验证对方发过来的证书文件,所有由同一个根证书签名的证书都应该能验证通过。

你可能感兴趣的:(go语言实现双向TLS认证的REST Service)