通過一個(gè)例子說(shuō)明客戶端如何驗(yàn)證HTTPS服務(wù)端的證書信息仁烹。
類型瀏覽器如何驗(yàn)證WEB服務(wù)器的證書信息贡珊。
生成服務(wù)器端證書零院,以及CA證書
# generate ca certificate
$ openssl genrsa -out ca-key.pem 2048
$ openssl req -new -x509 -days 365 -key ca-key.pem -out ca-cert.pem -subj "/CN=ca"
# generate server certificate
$ openssl genrsa -out server-key.pem 2048
$ openssl req -new -key server-key.pem -out server-csr.pem -subj "/CN=localhost"
$ openssl x509 -req -days 3650 -in server-csr.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem
生成服務(wù)端證書server-cert.pem登下,(注意證書的common name是localhost)引润,這個(gè)證書是通過CA證書ca-cert.pem簽名的崖媚。
服務(wù)器端代碼
$ cat server.go
package main
import (
"fmt"
"log"
"flag"
"net/http"
"crypto/tls"
"encoding/json"
"github.com/gorilla/mux"
)
var (
port int
hostname 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(&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 startHTTPSServer(address string, keyfile string, signcert string, router *mux.Router) {
s := &http.Server{
Addr: address,
Handler: router,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
}
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)
startHTTPSServer(address, keyfile, signcert, router)
fmt.Println("Exit main")
}
編譯運(yùn)行
$ go build
$ ./server -port 8080 -signcert ./server-cert.pem -key ./server-key.pem
運(yùn)行服務(wù)器在端口8080菱皆,這個(gè)服務(wù)器提供驗(yàn)證的證書是server-cert.pem,客戶端(瀏覽器將驗(yàn)證這個(gè)證書的有效性)劲腿。
客戶端代碼
$ cat client.js
fs = require('fs');
https = require('https');
options = {
hostname: 'localhost',
port : 8080,
path : '/service/hello',
method : 'GET',
ca : fs.readFileSync('ca-cert.pem')
};
req = https.request(options, (res) => {
console.log('statusCode:', res.statusCode);
console.log('headers:', res.headers);
res.on('data', (d) => {
process.stdout.write(d);
});
});
req.on('error', (e) => {
console.error(e);
});
req.end();
運(yùn)行
$ node client.js
{"hello":"world"}
完整的訪問地址格式就是 https://localhost:8080/service/hello
這里客戶端必須提供CA證書ca-cert.pem旭绒,用他來(lái)驗(yàn)證服務(wù)端的證書server-cert.pem是有效的。
另外這里客戶端訪問的地址是localhost焦人,這個(gè)值和服務(wù)器證書server-cert.pem的common name域是一樣的挥吵,否則驗(yàn)證就會(huì)失敗。
例如花椭,我們修改客戶端請(qǐng)求中hostname值真實(shí)到機(jī)器名(假定機(jī)器名為saturn)忽匈。
options = {
hostname: 'saturn',
port : 8080,
path : '/service/hello',
method : 'GET',
ca : fs.readFileSync('ca-cert.pem')
};
再運(yùn)行(https://saturn:8080/service/hello),會(huì)得到如下錯(cuò)誤:
$ node client.js
{ Error: Hostname/IP doesn't match certificate's altnames: "Host: saturn. is not cert's CN: localhost"
at Object.checkServerIdentity (tls.js:199:17)
at TLSSocket.<anonymous> (_tls_wrap.js:1098:29)
at emitNone (events.js:86:13)
at TLSSocket.emit (events.js:185:7)
at TLSSocket._finishInit (_tls_wrap.js:610:8)
at TLSWrap.ssl.onhandshakedone (_tls_wrap.js:440:38)
這說(shuō)明client使用URL的主機(jī)名和證書的Common Name域進(jìn)行比較矿辽,作為證書是否有效的一個(gè)證據(jù)丹允。
另外Common Name可以是一個(gè)短名(saturn)郭厌,也可以長(zhǎng)域名(saturn.yourcomp.com.cn),但不管短名還是長(zhǎng)名都必須是URL請(qǐng)求中的主機(jī)名一樣雕蔽。即
- 如果證書是saturn折柠,則必須用https://saturn:8080/service/hello訪問
- 如果證書是saturn.yourcomp.com.cn,則必須用https://saturn.yourcomp.com.cn:8080/service/hello訪問
SAN證書
SAN是另一種解決證書和URL主機(jī)名匹配的辦法萎羔。
SAN = subjectAltName = Subject Alternative Name
具體用法步驟:
修改openssl.cnf
[ req ]
req_extensions = v3_req
[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = saturn
DNS.2 = saturn.yourcomp.com.cn
生成證書
# generate ca certificate
openssl genrsa -out ca-key.pem 2048
openssl req -new -x509 -days 365 -key ca-key.pem -out ca-cert.pem -subj "/CN=ca"
# generate server certificate
openssl genrsa -out server-key.pem 2048
openssl req -new -key server-key.pem -out server-csr.pem -subj "/CN=localhost"
openssl x509 -req -days 3650 -in server-csr.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extensions v3_req -extfile openssl.cnf
對(duì)比和前面不使用SAN的差別液走。其實(shí)就是在使用CA根證書對(duì)服務(wù)器證書簽名的時(shí)候,指定extensions屬性贾陷。
運(yùn)行客戶端
options = {
hostname: 'localhost',
port : 8080,
path : '/service/hello',
method : 'GET',
ca : fs.readFileSync('ca-cert.pem')
};
$ node client.js
{ Error: Hostname/IP doesn't match certificate's altnames: "Host: localhost. is not in the cert's altnames: DNS:saturn"
看到了嗎缘眶?localhost已經(jīng)驗(yàn)證不過了,盡管他在common name的值沒有變化髓废,但是由于使用了SAN巷懈,證書驗(yàn)證的時(shí)候就使用SAN的值,而忽略common name的值慌洪。
使用客戶端saturn
options = {
hostname: 'saturn',
port : 8080,
path : '/service/hello',
method : 'GET',
ca : fs.readFileSync('ca-cert.pem')
};
$ node client.js
{"hello":"world"}
使用saturn就通過了顶燕。當(dāng)然使用saturn.yourcomp.com.cn也能通過。
總結(jié)
客戶端驗(yàn)證服務(wù)端證書分兩種情況:
- 不使用SAN冈爹,那么驗(yàn)證證書的common name和URL的主機(jī)名一致涌攻。
主機(jī)名是否是common name中的一個(gè)。 - 使用SAN频伤,那么驗(yàn)證證書的SAN值和URL的主機(jī)名一致恳谎。
主機(jī)名是否就是SAN值中的一個(gè)。