0%

[转]Go 密码学应用

Go Crypto

视频信息

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,因为必须的东西都内置了,从客户端到服务端。

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 比较安全。而且大部分网站也都支持。

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
  • 
    
  • 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 是验证的加密算法,因此别的都可以不选。

加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
)
func Encrypt(data []byte, key [32]byte) ([]byte, error) {
// 初始化 block cipher
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
// 设置 block cipher mode
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// 生成随机 nonce
nonce := make([]byte, gcm.NonceSize())
_, err = rand.Read(nonce)
if err != nil {
return nil, err
}
// 封装、返回
return gcm.Seal(nonce, nonce, data, nil), nil
}

解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
)
func Decrypt(ciphertext []byte, key [32]byte) (plaintext []byte, err error) {
// 初始化 block cipher
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
// 设置 block cipher mode
gcm, 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 可以使用这个比较安全的签名算法。

实现

生成密钥

1
2
3
4
5
6
7
8
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
)
func NewSigningKey() (*ecdsa.PrivateKey, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}

签名数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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() / 8
rBytes, 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
}

验证签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
"crypto/ecdsa"
"crypto/sha256"
"math/big"
)
// 验证成功返回 true,否则 false
func Verify(data, sig []byte, pub *ecdsa.PublicKey) bool {
digest := sha256.Sum256(data)
curveByteSize := pub.Curve.Params().P.BitLen() / 8
r, s := new(big.Int), new(big.Int)
r.SetBytes(signature[:curveByteSize])
s.SetBytes(signature[curveByteSize:])
return ecdsa.Verify(pub, digest[:], r, s)
}