偶然看到一篇文章:Go: Calculating public key hashes for public key pinning in curl,其中提到 cURL 的一个有趣的参数:--pinnedpubkey
。
使用过 cURL 的读者可能已经知道 -k
/ --insecure
参数,在使用 TLS 链接时(如访问 HTTPS 链接)时指定这个参数,将忽略对服务端返回的证书的合法性检查。通常访问使用自签名证书的链接时(典型场景如:非生产环境测试)会使用到它。
简单来说,TLS 1.3 中的主要流程是:
- 通过 ECDH(E) 协议在不可信的信道上进行对称加密密钥及参数的协商
- 通过 X509 证书进行对端的身份认证(多数情况由链接发起方进行,即客户端检查服务端,双向无非就是下面的流程互相做一次)
根据 man page 中的描述,这个 --pinnedpubkey
参数,不需要配合 -k
参数才能使用。个人理解就是 cURL 的一种自定义的、在上述 2
之外的一种额外的身份认证手段。
--pinnedpubkey
参数的值可以是以下任意一种:
- 文件路径,文件的内容需要含有
PEM
/DER
格式的公钥 - base64 编码的公钥的 sha256 哈希值加上固定的
sha256//
头部,多个值的话需要使用;
表示分隔
如果对 OpenSSH 的身份认证较为了解的读者应该会有一种熟悉感,是的,这类身份认证手段在概念上都是近似的。
OpenSSH 默认将远端主机的 SSH
Host Key
的哈希值存储在~/.ssh/known_hosts
文件中。
第二种按哈希方法的具体值怎么计算可以参考文章中的 Go 代码:
func calculatePublicKeyHashes(certs []*x509.Certificate) ([]string, error) {
hashes := make([]string, len(certs))
for i, cert := range certs {
derCert, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
if err != nil {
return nil, err
}
hash := sha256.New()
hash.Write(derCert)
hashes[i] = fmt.Sprintf("sha256//%s", base64.StdEncoding.EncodeToString(hash.Sum(nil)))
}
return hashes,nil
}
忽略代码中这个让人造成困惑的
derCert
变量名 (觉得叫pkInfoDERBytes
或者直接bs
更合适),理解计算的过程就行。
当然在实际使用中,还是使用文中提出的通过脚本调用 OpenSSL 进行计算的方式更为方便:
# Assuming default.crt is a PEM-encoded cert, this extracts the public key
# converts it to DER form, hashes it with SHA-256, then base64-encodes it
# and prepends "sha256//"
echo sha256//$(openssl x509 -in default.crt -pubkey -noout \
| openssl asn1parse -inform PEM -in - -noout -out - \
| openssl dgst -sha256 -binary - \
| openssl base64)
# Outputs something like sha256//Y/CGGnkaoZwUgOqArQs12llyoaX0bkjSIgHCPtXba+c=
使用公钥的哈希的设计在我看来完全可以直接使用 X509 中的 SKID,只是不知道 cURL 为什么选择了自定义格式的方式。
这个参数是在 7.39.0 (只支持 OpenSSL) 引入的。见 SSL: implement public key pinning
类似的
--proxy-pinnedpubkey
在 7.59.0 加入,见 curl: add –proxy-pinnedpubkey