视频笔记:Go 密码学应用 - George Tankersley
大桥下的蜗牛 · · 2111 次点击 · · 开始浏览视频信息 #
Go for Crypto Developers
by George Tankersley
at GopherCon 2016
https://www.youtube.com/watch?v=2r_KMzXB74w
幻灯地址:https://speakerdeck.com/gtank/crypto-for-go-developers
代码:https://github.com/gtank/cryptopasta
Don’t write your own crypto #
很多人把这句话误解为不要使用加密、不要使用任何密码学的技术,因为你不够聪明。No。完全不是这个意思。
这句话是说不要试图去发明创造那些加密类的算法。因为你不大可能会创造一个超过 AES 的加密算法、也不大可能会创造一个比 SHA 更好的 hash 算法。所以自己闭门造的算法一般意味着安全性的大大降低。
全世界能干这件事情的人不超过5个,而且他们今天都不在这里。另外一半都是 Daniel J. Bernstein 干的(开个玩笑,不过这人很牛,今天我们在用的很多加密的东西都是他设计、实现的)。
表面上好像这是个限制,其实这是个优势。因为我们不需要编写自己的加密算法,一切痛苦的工作都已经由别人做好了。我们只需要和搭积木一样去使用这些密码学工具就好了。
经常听到这样的建议 #
- 对传输中的数据用 TLS
- 对静止的数据用 GPG
TLS #
Go 可以很容易使用 TLS,因为必须的东西都内置了,从客户端到服务端。
客户端
123456789101112131415161718
var minimalTLSConfig = &tls.Config{MinVersion: tls.VersionTLS12,}var tlsTransport = &http.Transport{TLSClientConfig: minimalTLSConfig,}var httpClient = &http.Client{Transport: tlsTransport,Timeout: 10 * time.Second,}func MakeRequest() error {resp, err := httpClient.Get("https://www.google.com")if err != nil {return err}// have fun}
这里最重要的是 tls.VersionTLS12,因为低于 1.2 的话,会导致 Go 使用一些不安全的低版本的实现,所以这里限定 1.2 比较安全。而且大部分网站也都支持。
服务端
12345678910111213141516171819
var minimalTLSConfig = &tls.Config{MinVersion: tls.VersionTLS12,PreferServerCipherSuites: true,}var srv = &http.Server{Addr: "localhost:8080",TLSConfig: minimalTLSConfig,}func handleReq(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello, world")}func main() {http.HandleFunc("/", handleReq)err := srv.ListenAndServeTLS("cert.pem", "key.pem")if err != nil {log.Fatal(err)}}
这里和客户端一样,限定最小版本是 1.2,并且多了一个额外的参数,要求以服务器的 Cipher 优先,因此不按照客户端给的选择,而是按照Go服务端给的选择。因为 Go 的 TLS 包中有大量针对各种安全问题的调整,因此选择加密包的话,遵循 Go 内部的决定是最好的。
GPG #
GPG 是为了人和人之间的交流信息,而不是机器和机器之间交流信息的。
那么如何安全的使用 GPG 呢?答案就是不用GPG。因为作者觉得 GPG 太过阴谋论了,陷入了很多本不需要过多注意的区域,即使那么做了,也不见得更安全。
这个 Talk 不讲 TLS 和 GPG #
因为当你实现安全的信息系统的时候,通常不会用到这两个东西。TLS 和 GPG 有他们应用的场合,但是对于每天的密码学工作来说,基本都不用这两个工具:
- 对文件计算散列
- 生成随机 ID
- API 验证
- 网站密码存储
- 签名、加密 cookies
- JWT
- 签名更新
在 Go 的 crypto 包里的算法可不都是好的算法 #
加密 #
下列划掉的算法都不应该再使用了:
(削除) DES (削除ここまで)(削除) 3DES (削除ここまで)(削除) RC4 (削除ここまで)(削除) TEA (削除ここまで)(削除) XTEA (削除ここまで)(削除) Blowfish (削除ここまで)- ✔️ Twofish
(削除) CAST5 (削除ここまで)- ✔️ Salsa20
- ✔️ AES
只有 Twofish、Salsa20 和 AES 还算是安全的加密算法,但是 AES 在大部分的计算机上都有硬件加速。因此只剩下一个 AES 是最佳加密算法。
怎么使用 AES #
要注意,aesCipher.Encrypt() 只会加密前16个字节。不少人掉到这个坑里了,结果不知道为啥就前几个字符是密文,后面全都是明文。解决办法就是不直接用AES,通过 Mode 来使用。
Block cipher mode 一样有很多种选择:
(削除) CBC (削除ここまで)(削除) CFB (削除ここまで)(削除) CTR (削除ここまで)(削除) OFB (削除ここまで)- ✔️ GCM
这里只有 GCM 是验证的加密算法,因此别的都可以不选。
加密 #
12345678910111213141516171819202122232425
import ("crypto/aes""crypto/cipher""crypto/rand")func Encrypt(data []byte, key [32]byte) ([]byte, error) {// 初始化 block cipherblock, err := aes.NewCipher(key[:])if err != nil {return nil, err}// 设置 block cipher modegcm, err := cipher.NewGCM(block)if err != nil {return nil, err}// 生成随机 noncenonce := make([]byte, gcm.NonceSize())_, err = rand.Read(nonce)if err != nil {return nil, err}// 封装、返回return gcm.Seal(nonce, nonce, data, nil), nil}
解密 #
1234567891011121314151617181920212223
import ("crypto/aes""crypto/cipher""crypto/rand")func Decrypt(ciphertext []byte, key [32]byte) (plaintext []byte, err error) {// 初始化 block cipherblock, err := aes.NewCipher(key[:])if err != nil {return nil, err}// 设置 block cipher modegcm, err := cipher.NewGCM(block)if err != nil {return nil, err}// 返回解开的包,注意这里的 nonce 是直接取的。return gcm.Open(nil,ciphertext[:gcm.NonceSize()],ciphertext[gcm.NonceSize():],nil,)}
哈希散列 #
将一大段数据使用 Hash 算法,希望得到一串数值,这串数值可以反映你的这段数据,任何数据变化,这串数值都不同。而且希望无法从这串数值反推数据。这就是密码学 Hash 函数要保证的。(注意:哈希不等于加密,很多人这点容易搞混)
同样 Hash 函数一样有很多选择,一样是大部分都不用:
(削除) MD4 (削除ここまで)(削除) MD5 (削除ここまで)(削除) RIPEMD160 (削除ここまで)(削除) SHA1 (削除ここまで)- ✔️ SHA2
- ✔️ SHA3
选择 SHA3 并不是因为它比 SHA2 更新更好,而是因为它不同于 SHA1 和 SHA2。几年前密码学家已经开始担心,因为MD以及SHA的哈希算法本质太相似了,那么一旦这个依赖出现问题,就意味着这个体系的不再安全。因此开始寻找一种不同的算法。经过竞赛、挑选,最终 SHA3 脱颖而出。他本身是个很出色的 Hash 算法,同时其设计和之前的这几个算法完全不一样。
但是由于 SHA3 并不被广泛支持,所以如果你明确知道你可以用 SHA3,那么就用 SHA3。其它情况用 SHA2。
但是和加密一样,我们不应该直接使用 Hash 算法。因为可能会面临一系列的攻击:
- Length extension
- Rainbow tables
- Small number of possibilities (phone numbers)
- Salt? Peper?
我们应该使用
HMAC,而不要直接用 Hash
实现 Hash #
1234567891011121314151617181920
import ("crypto/hmac""crypto/sha512")func Hash(tag string, data []byte) []byte {h := hmac.New(sha512.New512_256, []byte(tag))h.Write(data)return h.Sum(nil)}func ExampleHash() error {tag := "hashing file for storage key"contents, err := ioutil.ReadFile("testfile")if err != nil {return error}digest := Hash(tag, contents)fmt.Println(hex.EncodeToString(digest))}// Output:// 9f4c795d8ae5e207f19184ccebee6a606c1fdfe509c793614006d613580f03e1
Hash 密码 #
一般东西的 Hash 用刚才的就行了,但是除了密码Hash。密码 Hash 和数据 Hash 的特征完全不同。
- 数据 Hash 希望的是 Hash 算法越快越好
- 而密码 Hash 则希望 Hash 算法越慢越好
过快的密码哈希会导致暴力破解的成本降低。因此密码哈希需要特殊算法。
使用 bcrypt #
1234567891011121314151617
import ("golang.org/x/crypto/bcrypt")func HashPassword(password []byte) ([]byte, error) {return bcrypt.GenerateFromPassword(password, 14)}func CheckPasswordHash(hash, password []byte) error {return bcrypt.CompareHashAndPassword(hash, password)}func Example() {myPassword := []byte("password")hashed, err := HashPassword(myPassword)if err != nil {return}fmt.Println(string(hashed))}
14 是计算量的复杂度,14 是个比较好的值,如果觉得性能无法接受,可以降到 12,但是不要再低了。
签名 #
首先是有一对密钥,一个是公钥、一个是私钥。任何拥有私钥的人可以对一段信息签名,而所有拥有公钥的人都可以来验证这个消息确实是由那个人签名的。
通过签名可以确保两件事情:
- 消息未曾被篡改
- 是谁发出的这个消息
和前面一样,Go 有很多签名算法可以选择:
- RSA
(削除) PKCS1v15 (削除ここまで)- PSS
- ECDSA
- ✔️ P256
(削除) P385 (削除ここまで)(削除) P521 (削除ここまで)
- Ed25519
这次和前面不同,签名算法的安全更多的不是取决于算法选择,而是取决于你是怎么使用的。
比如这里比较推荐使用 ECDSA/P256,但是要注意,当初 PS3 被黑,被解出私钥就是用的这个算法,当时是由于那个算法实现是非常烂的。幸运的是 Go 没这个问题。所以相对于其他语言,Go 可以使用这个比较安全的签名算法。
实现 #
生成密钥
12345678
import ("crypto/ecdsa""crypto/elliptic""crypto/rand")func NewSigningKey() (*ecdsa.PrivateKey, error) {return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)}
签名数据
123456789101112131415
func Sign(data []byte, priv *ecdsa.PrivateKey) ([]byte, error) {digest := sha256.Sum256(data)r, s, err := ecdsa.Sign(rand.Reader, priv, digest[:])if err != nil {return nil, err}// encode the signature {R, S}params := priv.Curve.Params()curveByteSize := params.P.BitLen() / 8rBytes, sBytes := r.Bytes(), s.Bytes()signature := make([]byte, curveByteSize * 2)copy(signature[curveByteSize - len(rBytes):], rBytes)copy(signature[curveByteSize*2 - len(sBytes):], sBytes)return signature, nil}
验证签名
123456789101112131415
import ("crypto/ecdsa""crypto/sha256""math/big")// 验证成功返回 true,否则 falsefunc Verify(data, sig []byte, pub *ecdsa.PublicKey) bool {digest := sha256.Sum256(data)curveByteSize := pub.Curve.Params().P.BitLen() / 8r, s := new(big.Int), new(big.Int)r.SetBytes(signature[:curveByteSize])s.SetBytes(signature[curveByteSize:])return ecdsa.Verify(pub, digest[:], r, s)}
有疑问加站长微信联系(非本文作者)
入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889
关注微信- 请尽量让自己的回复能够对别人有帮助
- 支持 Markdown 格式, **粗体**、~~删除线~~、
`单行代码` - 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
- 图片支持拖拽、截图粘贴等方式上传
收入到我管理的专栏 新建专栏
视频信息 #
Go for Crypto Developers
by George Tankersley
at GopherCon 2016
https://www.youtube.com/watch?v=2r_KMzXB74w
幻灯地址:https://speakerdeck.com/gtank/crypto-for-go-developers
代码:https://github.com/gtank/cryptopasta
Don’t write your own crypto #
很多人把这句话误解为不要使用加密、不要使用任何密码学的技术,因为你不够聪明。No。完全不是这个意思。
这句话是说不要试图去发明创造那些加密类的算法。因为你不大可能会创造一个超过 AES 的加密算法、也不大可能会创造一个比 SHA 更好的 hash 算法。所以自己闭门造的算法一般意味着安全性的大大降低。
全世界能干这件事情的人不超过5个,而且他们今天都不在这里。另外一半都是 Daniel J. Bernstein 干的(开个玩笑,不过这人很牛,今天我们在用的很多加密的东西都是他设计、实现的)。
表面上好像这是个限制,其实这是个优势。因为我们不需要编写自己的加密算法,一切痛苦的工作都已经由别人做好了。我们只需要和搭积木一样去使用这些密码学工具就好了。
经常听到这样的建议 #
- 对传输中的数据用 TLS
- 对静止的数据用 GPG
TLS #
Go 可以很容易使用 TLS,因为必须的东西都内置了,从客户端到服务端。
客户端
123456789101112131415161718
var minimalTLSConfig = &tls.Config{MinVersion: tls.VersionTLS12,}var tlsTransport = &http.Transport{TLSClientConfig: minimalTLSConfig,}var httpClient = &http.Client{Transport: tlsTransport,Timeout: 10 * time.Second,}func MakeRequest() error {resp, err := httpClient.Get("https://www.google.com")if err != nil {return err}// have fun}
这里最重要的是 tls.VersionTLS12,因为低于 1.2 的话,会导致 Go 使用一些不安全的低版本的实现,所以这里限定 1.2 比较安全。而且大部分网站也都支持。
服务端
12345678910111213141516171819
var minimalTLSConfig = &tls.Config{MinVersion: tls.VersionTLS12,PreferServerCipherSuites: true,}var srv = &http.Server{Addr: "localhost:8080",TLSConfig: minimalTLSConfig,}func handleReq(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello, world")}func main() {http.HandleFunc("/", handleReq)err := srv.ListenAndServeTLS("cert.pem", "key.pem")if err != nil {log.Fatal(err)}}
这里和客户端一样,限定最小版本是 1.2,并且多了一个额外的参数,要求以服务器的 Cipher 优先,因此不按照客户端给的选择,而是按照Go服务端给的选择。因为 Go 的 TLS 包中有大量针对各种安全问题的调整,因此选择加密包的话,遵循 Go 内部的决定是最好的。
GPG #
GPG 是为了人和人之间的交流信息,而不是机器和机器之间交流信息的。
那么如何安全的使用 GPG 呢?答案就是不用GPG。因为作者觉得 GPG 太过阴谋论了,陷入了很多本不需要过多注意的区域,即使那么做了,也不见得更安全。
这个 Talk 不讲 TLS 和 GPG #
因为当你实现安全的信息系统的时候,通常不会用到这两个东西。TLS 和 GPG 有他们应用的场合,但是对于每天的密码学工作来说,基本都不用这两个工具:
- 对文件计算散列
- 生成随机 ID
- API 验证
- 网站密码存储
- 签名、加密 cookies
- JWT
- 签名更新
在 Go 的 crypto 包里的算法可不都是好的算法 #
加密 #
下列划掉的算法都不应该再使用了:
(削除) DES (削除ここまで)(削除) 3DES (削除ここまで)(削除) RC4 (削除ここまで)(削除) TEA (削除ここまで)(削除) XTEA (削除ここまで)(削除) Blowfish (削除ここまで)- ✔️ Twofish
(削除) CAST5 (削除ここまで)- ✔️ Salsa20
- ✔️ AES
只有 Twofish、Salsa20 和 AES 还算是安全的加密算法,但是 AES 在大部分的计算机上都有硬件加速。因此只剩下一个 AES 是最佳加密算法。
怎么使用 AES #
要注意,aesCipher.Encrypt() 只会加密前16个字节。不少人掉到这个坑里了,结果不知道为啥就前几个字符是密文,后面全都是明文。解决办法就是不直接用AES,通过 Mode 来使用。
Block cipher mode 一样有很多种选择:
(削除) CBC (削除ここまで)(削除) CFB (削除ここまで)(削除) CTR (削除ここまで)(削除) OFB (削除ここまで)- ✔️ GCM
这里只有 GCM 是验证的加密算法,因此别的都可以不选。
加密 #
12345678910111213141516171819202122232425
import ("crypto/aes""crypto/cipher""crypto/rand")func Encrypt(data []byte, key [32]byte) ([]byte, error) {// 初始化 block cipherblock, err := aes.NewCipher(key[:])if err != nil {return nil, err}// 设置 block cipher modegcm, err := cipher.NewGCM(block)if err != nil {return nil, err}// 生成随机 noncenonce := make([]byte, gcm.NonceSize())_, err = rand.Read(nonce)if err != nil {return nil, err}// 封装、返回return gcm.Seal(nonce, nonce, data, nil), nil}
解密 #
1234567891011121314151617181920212223
import ("crypto/aes""crypto/cipher""crypto/rand")func Decrypt(ciphertext []byte, key [32]byte) (plaintext []byte, err error) {// 初始化 block cipherblock, err := aes.NewCipher(key[:])if err != nil {return nil, err}// 设置 block cipher modegcm, err := cipher.NewGCM(block)if err != nil {return nil, err}// 返回解开的包,注意这里的 nonce 是直接取的。return gcm.Open(nil,ciphertext[:gcm.NonceSize()],ciphertext[gcm.NonceSize():],nil,)}
哈希散列 #
将一大段数据使用 Hash 算法,希望得到一串数值,这串数值可以反映你的这段数据,任何数据变化,这串数值都不同。而且希望无法从这串数值反推数据。这就是密码学 Hash 函数要保证的。(注意:哈希不等于加密,很多人这点容易搞混)
同样 Hash 函数一样有很多选择,一样是大部分都不用:
(削除) MD4 (削除ここまで)(削除) MD5 (削除ここまで)(削除) RIPEMD160 (削除ここまで)(削除) SHA1 (削除ここまで)- ✔️ SHA2
- ✔️ SHA3
选择 SHA3 并不是因为它比 SHA2 更新更好,而是因为它不同于 SHA1 和 SHA2。几年前密码学家已经开始担心,因为MD以及SHA的哈希算法本质太相似了,那么一旦这个依赖出现问题,就意味着这个体系的不再安全。因此开始寻找一种不同的算法。经过竞赛、挑选,最终 SHA3 脱颖而出。他本身是个很出色的 Hash 算法,同时其设计和之前的这几个算法完全不一样。
但是由于 SHA3 并不被广泛支持,所以如果你明确知道你可以用 SHA3,那么就用 SHA3。其它情况用 SHA2。
但是和加密一样,我们不应该直接使用 Hash 算法。因为可能会面临一系列的攻击:
- Length extension
- Rainbow tables
- Small number of possibilities (phone numbers)
- Salt? Peper?
我们应该使用
HMAC,而不要直接用 Hash
实现 Hash #
1234567891011121314151617181920
import ("crypto/hmac""crypto/sha512")func Hash(tag string, data []byte) []byte {h := hmac.New(sha512.New512_256, []byte(tag))h.Write(data)return h.Sum(nil)}func ExampleHash() error {tag := "hashing file for storage key"contents, err := ioutil.ReadFile("testfile")if err != nil {return error}digest := Hash(tag, contents)fmt.Println(hex.EncodeToString(digest))}// Output:// 9f4c795d8ae5e207f19184ccebee6a606c1fdfe509c793614006d613580f03e1
Hash 密码 #
一般东西的 Hash 用刚才的就行了,但是除了密码Hash。密码 Hash 和数据 Hash 的特征完全不同。
- 数据 Hash 希望的是 Hash 算法越快越好
- 而密码 Hash 则希望 Hash 算法越慢越好
过快的密码哈希会导致暴力破解的成本降低。因此密码哈希需要特殊算法。
使用 bcrypt #
1234567891011121314151617
import ("golang.org/x/crypto/bcrypt")func HashPassword(password []byte) ([]byte, error) {return bcrypt.GenerateFromPassword(password, 14)}func CheckPasswordHash(hash, password []byte) error {return bcrypt.CompareHashAndPassword(hash, password)}func Example() {myPassword := []byte("password")hashed, err := HashPassword(myPassword)if err != nil {return}fmt.Println(string(hashed))}
14 是计算量的复杂度,14 是个比较好的值,如果觉得性能无法接受,可以降到 12,但是不要再低了。
签名 #
首先是有一对密钥,一个是公钥、一个是私钥。任何拥有私钥的人可以对一段信息签名,而所有拥有公钥的人都可以来验证这个消息确实是由那个人签名的。
通过签名可以确保两件事情:
- 消息未曾被篡改
- 是谁发出的这个消息
和前面一样,Go 有很多签名算法可以选择:
- RSA
(削除) PKCS1v15 (削除ここまで)- PSS
- ECDSA
- ✔️ P256
(削除) P385 (削除ここまで)(削除) P521 (削除ここまで)
- Ed25519
这次和前面不同,签名算法的安全更多的不是取决于算法选择,而是取决于你是怎么使用的。
比如这里比较推荐使用 ECDSA/P256,但是要注意,当初 PS3 被黑,被解出私钥就是用的这个算法,当时是由于那个算法实现是非常烂的。幸运的是 Go 没这个问题。所以相对于其他语言,Go 可以使用这个比较安全的签名算法。
实现 #
生成密钥
12345678
import ("crypto/ecdsa""crypto/elliptic""crypto/rand")func NewSigningKey() (*ecdsa.PrivateKey, error) {return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)}
签名数据
123456789101112131415
func Sign(data []byte, priv *ecdsa.PrivateKey) ([]byte, error) {digest := sha256.Sum256(data)r, s, err := ecdsa.Sign(rand.Reader, priv, digest[:])if err != nil {return nil, err}// encode the signature {R, S}params := priv.Curve.Params()curveByteSize := params.P.BitLen() / 8rBytes, sBytes := r.Bytes(), s.Bytes()signature := make([]byte, curveByteSize * 2)copy(signature[curveByteSize - len(rBytes):], rBytes)copy(signature[curveByteSize*2 - len(sBytes):], sBytes)return signature, nil}
验证签名
123456789101112131415
import ("crypto/ecdsa""crypto/sha256""math/big")// 验证成功返回 true,否则 falsefunc Verify(data, sig []byte, pub *ecdsa.PublicKey) bool {digest := sha256.Sum256(data)curveByteSize := pub.Curve.Params().P.BitLen() / 8r, s := new(big.Int), new(big.Int)r.SetBytes(signature[:curveByteSize])s.SetBytes(signature[curveByteSize:])return ecdsa.Verify(pub, digest[:], r, s)}