Go代码打通HTTPs
TL;DR 手工创建CA证书链,手写代码打通HTTPs的两端
HTTPs最近是一个重要的话题,同时也是一个有点难懂的话题。所以网上有大量的HTTPs/TLS/SSL的教程。关于这些的原理,这里不做讲解,有兴趣的可以自行搜索。
本文介绍一个自己创建证书,并编写 Go 代码实现 client/server 两端的过程。从实践的角度帮助理解。
构建 CA 证书链 我们首先要创建 client/server 使用的证书。创建证书的方法有很多种:有不怕麻烦,直接通过 openssl 创建的,有通过 cfssl 创建的。这里要介绍的是我认为最简单的一种:tls-gen
tls-gen是一个用 Python 编写的、非常易用的工具。它定义了三种 profile。这里我们选择最简单的一种:一个根证书和一组证书、私钥对。
在 shell 里面执行一下的命令:
- git clone https://github.com/michaelklishin/tls-gen
- cd tls-gen/basic
- make CN=www.mytestdomain.io 就这样,我们就为域名 www.mytestdomain.io 创建了一套证书。观察一下当前路径的内容,我们会发现两个新的目录:testca 和 server。前者里面存放了刚刚创建的根证书 (root CA),后者里面存放了我们之后的服务程序要用的的证书和私钥。
testca/
cacert.pemserver/
cert.pem
key.pem
编写服务 接下来开始写代码。Go 对 TLS 的支持还是比较完备的,也比较简单。以下是服务器端的代码 (server.go):
func HelloServer(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("This is an example server.n"))
}
func main() {
http.HandleFunc("/hello", HelloServer)
err := http.ListenAndServeTLS(":1443", "server/cert.pem", "server/key.pem", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
可以看到我们创建了一个 HTTP 服务,这个服务监听 1443 端口并且只处理一个路径 /hello。然后调用了下面这个函数来监听 1443 端口。注意我们给出了之前创建的服务的证书和私钥 - 这样就保证了HTTP会用加密的方式来传输。
ListenAndServeTLS(addr, certFile, keyFile string, handler Handler)
运行服务程序:
go run server.go
访问HTTPs服务 假定我们的服务程序是运行在本地的。我们先改一下 /etc/hosts 来配置域名解析:
# echo 127.0.0.1 www.mytestdomain.io >> /etc/hosts
我们用以下的代码 (client.go) 来访问服务:
func main() { client := &http.Client{}
resp, err := client.Get("https://www.mytestdomain.io:1443/hello")
if err != nil {
panic("failed to connect: " + err.Error())
}
content, _ := ioutil.ReadAll(resp.Body)
s := strings.TrimSpace(string(content))
fmt.Println(s)
}
运行 go run client.go,只能得到这样的错误:
panic: failed to connect: Get https://www.mytestdomain.io:1443/hello: x509: certificate signed by unknown authorit
这是因为系统不知道如何来处理这个 self signed 证书。
各个 OS 添加根证书的方法是不同的。对于 Linux 系统 (以 Ubuntu 为例) 来说,把证书文件放到相应的目录即可:
# sudo cp testca/cacert.pem /etc/ssl/certs
如果是 macOS,可以用一下的命令:
# sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain testca/cacert.pem
上面的方法会把我们手工创建的 root CA 添加到系统所已知的列表里面。这样一来,所有用该 root CA 创建的证书都可以被认证了。
现在我们再次运行刚才那个程就会成功的获得服务端的响应了:
This is an example server.
另一种访问方法 假如只是一个普通的用户,没有 root/sudo 权限,不就无法做上面的操作了吗?这种情况下还有另外一种做法: 把 root CA 放置在代码里面。
在上面的 client.go 里面添加这么几行代码:
func main() {
roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM([]byte(rootPEM))
if !ok { panic("failed to parse root certificate")
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: roots},
}
client := &http.Client{Transport: tr} // ...
其中的 rootPEM 就是 testca/cacert.pem 的内容
var rootPEM = `-----BEGIN CERTIFICATE-----MIIDAjCCAeqgAwIBA
gIJAL2faqa73yLvMA0GCSqGSIb3DQEBCwUAMDExIDAeBgNVBAMMF1RMU0dl
blNlbGZTaWduZWR0Um9vdENBMQ0wCwYDVQQHDAQkJCQkMB4XDTE4MDIwNTA
5Mzc0NVoXDTI4MDIwMzA5Mzc0NVowMTEgMB4GA1UEAwwXVExTR2VuU2VsZl
NpZ25lZHRSb290Q0ExDTALBgNVBAcMBCQkJCQwggEiMA0GCSqGSIb3DQEBA
QUAA4IBDwAwggEKAoIBAQC9eO6Tam4XFDUbK9FAStAg29teYeKtt8WEJvK
GB50xMfXO2pD0StsXhKrspXBYck0FwKIBsTLr97w7dSqa64z3U2V2Borog
FzoEE4JH2sydYGAQqNAqezGx8VZnQVRyZEBifRPebR4WVD5GtXYe+MnSkH
PIgsG0QG0SaiSfMl05dSJHoE9T9Kly9fH6yED88++OYjZZRGKOf2THpQlX
JjF3iwCDLkwz9Z/kjmpK/rR0SEhtanf7bOgGs3OoFmX4DvmFJXoriVUC9jc
j0Z4oX3Ld81XXyd4FJkpKvdKDhYkqcugFgERqdBeRDM+MA38YooKHZh0klL
2EThNXJxM0r1vAgMBAAGjHTAbMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgE
GMA0GCSqGSIb3DQEBCwUAA4IBAQBEqp0ON1A/pCKFztfKuzdW+9pauE8dl6Ij
3++dt6AqW5QYFLOEFQwoMBOkGChGQDxHkakyaA0DfGe5JntMH0yYyZnr4kfs+
AcY6P+2PfgrgVBqadhR6uAGOBaXDW7dlllqIJJ8NRInA/fTDYXMxBJbFrcj2c
GIYVPvAbrosZ5L/YdAdVM76V8uuk8Hmmy5zRQj+gWt/jDkYWFrp0b6k3FBXvM7
+nhqAIdyMjLioAdYwFpPglGj3xHXS5neWjyUDlAYISNe+PKMERSeDrptyDE+ljz
l77hvvfZD9OPhXbDkAeVU/NaDwHG/G5HDVdNbg/FZ6ueevF34Xuzejm3lrdJm-----E
ND CERTIFICATE-----`
也就是说,我们用准备好的 root CA 的内容产生了一个新的 http transport。
运行一下 go run client.go。成功!
This is an example server.
总结
一对 HTTPs client/server 程序中需要一个共同的 root CA。服务器端需要该 root CA
创建的 CA/私钥对。
这里用的是 Go 语言来实现,其它的语言过程也类似。
- Comparison of Apache Stream Processing Frameworks: Part 1
- 【LeetCode】关关的刷题日记23——Leetcode 66. Plus One
- Codeforces Round #434 (Div. 2, based on Technocup 2018 Elimination Round 1)&&Codeforces 861A k-roun
- 【Java数据结构学习笔记之二】Java数据结构与算法之栈(Stack)实现
- 【Java数据结构学习笔记之三】Java数据结构与算法之队列(Queue)实现
- Comparison of Apache Stream Processing Frameworks: Part 2
- 2017 Multi-University Training Contest - Team 9 1005&&HDU 6165 FFF at Valentine【强联通缩点+拓扑排序】
- 2017 Multi-University Training Contest - Team 9 1004&&HDU 6164 Dying Light【数学+模拟】
- Python3选择排序
- 【DeepMind 公开课-深度强化学习教程代码实战01】迭代法评估4*4方格世界下的随机策略
- Codeforces Round #434 (Div. 2, based on Technocup 2018 Elimination Round 1)&&Codeforces 861C Did yo
- Codeforces Round #434 (Div. 2, based on Technocup 2018 Elimination Round 1)&&Codeforces 861B Which
- 信用卡安全问题:被用户忽视的识别码
- Python3快速排序
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Devtools 老师傅养成[5] - Network 面板
- android实现上传本地图片到网络功能
- android音乐播放简单实现的简单示例(MediaPlayer)
- Android实现桌面悬浮小火箭效果
- Android数据库中事务操作方法之银行转账示例
- Android实现QQ登录功能
- Devtools 老师傅养成[6] - Performance 面板
- RecyclerView上拉加载封装代码
- Devtools 老师傅养成[7] - Memory 内存
- Android实现简单的城市列表功能
- Android Animation之TranslateAnimation(平移动画)
- Android自定义View实现箭头沿圆转动实例代码
- Android 中Context的使用方法详解
- Android自定义水平渐变进度条
- Android+SQLite数据库实现的生词记事本功能实例