主题
密码学应用:先决定要保护什么,再选算法
保存用户密码、给数据库字段加密、校验 webhook、建立 HTTPS 连接,虽然都用到了密码学,目标却不同。把 SHA-256 换成 AES 不能自动让密码存储更安全,给数据做 Base64 也没有保密效果。
本文围绕四个问题展开:以后需不需要还原原文,谁能验证内容,谁能生成可信内容,以及密钥泄漏后如何恢复。代码使用标准密码学实现,演示密码校验、认证加密、MAC、签名和证书验证,不设计新算法或新密码协议。
编码、哈希、加密:区别在它们承诺什么
| 操作 | 是否需要秘密 | 能否按设计还原输入 | 提供的主要能力 | 不能直接提供什么 |
|---|---|---|---|---|
| Base64 编码 | 不需要 | 可以直接解码 | 把字节表示成适合传输的文本 | 保密、身份、抗篡改 |
| SHA-256 等密码学哈希 | 不需要 | 没有解密步骤,但可猜输入再比较 | 摘要、完整性比较的组件 | 单独证明来源、抵抗低熵密码猜测 |
| 密码 KDF,例如 Argon2id | 盐公开,通常不依赖秘密 | 不解密;用候选密码重新计算 | 提高离线猜密码的时间和内存成本 | 让弱密码不可猜、阻止在线撞库 |
| AEAD 加密 | 需要密钥 | 持密钥方可解密 | 保密,并验证密文与附加数据完整性 | 自动判断业务授权或防止合法密文重放 |
| HMAC | 需要共享秘密 | 不用于还原 | 共享密钥圈内的消息完整性与认证 | 区分所有共享密钥持有者 |
| 数字签名 | 私钥签名,公钥验证 | 不用于还原 | 验证者不持有签名能力 | 加密消息、自动证明现实身份 |
Base64 的实际输出很直观:
go
base64.StdEncoding.EncodeToString([]byte("hello"))text
base64: aGVsbG8=知道编码规则的任何人都能还原它。哈希虽然没有对应的“解密”,攻击者仍能对常见密码逐个计算并比对,所以“不可逆”不等于“难猜”。哈希、KDF、AEAD 与签名是组件,只有放到合适的威胁模型里才有意义。RFC 4648、Go SHA-256、RFC 5116 AEAD
密码存储:让每次离线猜测都付出代价
登录系统只需要判断输入密码是否匹配,不需要取回原密码,因此通常存密码 KDF 的结果,而不是加密后的原密码。数据库一旦泄漏,攻击者可以离线尝试,不受登录接口限流约束。MD5、SHA-1、SHA-256 直接算一次都太快;给快速哈希简单加盐,仍然没有获得现代密码 KDF 的工作因子和内存成本。
截至 2026-10-13 查证,OWASP 密码存储指南 将 Argon2id 的一组最低配置列为 19 MiB、2 次迭代、并行度 1;bcrypt 主要用于遗留系统,工作因子至少 10,并注意 72 字节输入限制。最低值不是服务器的最终调优结果,也不是未来永远不变的建议。
| 选择 | 主要调节项 | 实际好处 | 部署时的代价与边界 |
|---|---|---|---|
| Argon2id | 内存、迭代次数、并行度 | 同时增加猜测的计算和内存负担 | 登录并发会放大内存占用,必须按预算压测 |
| bcrypt | cost,增长会增加计算工作 | 生态成熟,便于维护历史哈希 | 有输入长度边界,不具备同样的可调内存参数 |
| scrypt | N、r、p | Argon2id 不可用时的内存困难选择 | 参数约束、峰值内存与库支持需要验证 |
| PBKDF2-HMAC | PRF 与迭代次数 | 某些合规环境中的既定方案 | 需要当前推荐参数及合规实现,不能只看算法名称 |
Argon2 的 RFC 给出了更高资源预算的推荐配置,例如 64 MiB、3 次迭代、4 条 lane 的受限内存选项;它与 OWASP 的最低配置不是同一层次的承诺。应该先确定可承受的单次时间、并发量和内存,再在目标机器上测量,而不是从两张表里挑一个更好看的数字。RFC 9106 §4、§7.4
盐、参数、摘要一起保存,秘密另有用途
盐是每条密码记录独立生成的随机值,可以公开保存。它使相同密码在不同记录上得到不同结果,也使预计算结果不能直接跨用户复用。记录应包含算法、版本、参数、盐和结果,便于日后升级。额外的 pepper 是服务端秘密,应与密码数据库分离管理;若它泄漏,可能需要重置密码,不能把它当作替代 KDF 的捷径。
正在绘制图示…
查看图源代码
flowchart LR
P["用户输入密码"] --> K["KDF:固定且受限的参数"]
S["记录中的随机盐"] --> K
K --> C["常数时间比较"]
H["保存的 KDF 结果"] --> C
C --> A{"匹配?"}
A -- "否" --> N["统一失败响应与限流"]
A -- "是" --> U["必要时用新参数重算并更新"]
本例固定 golang.org/x/crypto v0.58.0,用 argon2.IDKey 验证机制。参数的内存单位是 KiB,19*1024 才是 19 MiB;返回值长度这里设为 32 字节。它返回原始派生结果,不会自动替你生成盐或保存完整的密码记录格式。固定版本 argon2 实现
go
argon2.IDKey(password, salt, 2, 19*1024, 1, 32)程序使用随机 16 字节盐,分别检验正确密码、错误密码、相同密码换盐,以及 bcrypt 校验。实际输出:
text
argon-good=true argon-wrong=false different-salts=true bcrypt-good=true
bcrypt-73-bytes-rejected: true最后一行来自向 Go bcrypt 传入 73 个 ASCII 字节。这个库返回 ErrPasswordTooLong;不要在调用前悄悄截断密码,也不要把“72 字节”误写成“72 个汉字”。采用预哈希兼容长密码涉及额外协议与编码风险,不能随手拼接一层 SHA 当修复。固定版本 bcrypt 源码
生产验证器还要限制从密码记录解析出的参数范围,防止异常记录要求分配巨大内存;为登录并发做容量预算并设置限流,避免昂贵 KDF 变成拒绝服务放大器。不存在的账号也应避免明显泄漏与已有账号不同的验证路径。本文没有实现密码记录解析器、参数迁移、账号枚举防护或并发容量测试;没有测出适合你的生产参数。
加密数据:默认把完整性一起考虑
对称加密由共享密钥完成,适合大量数据。非对称密码学把公钥和私钥分开,常用于签名、密钥协商或封装密钥;不要把“公钥算法”当作直接加密任意长文件的通用 API。常见系统用对称数据密钥加密内容,再用 KMS 管理的主密钥保护数据密钥,这叫信封加密。NIST SP 800-38D、AWS KMS 信封加密说明
仅加密而不验证完整性,可能让被修改的数据继续进入业务。AEAD 把加密和认证组合在一起:输入是密钥、nonce、明文和可选附加认证数据 AAD;输出通常包含密文和认证标签。AAD 会参与验证,但不会被加密,适合绑定租户 ID、记录 ID、版本等不需保密的上下文。RFC 5116 §2
nonce 重复,不是“小概率多解出一条”那么简单
GCM 要求同一密钥下按其规则避免重复 nonce;重用可能泄漏明文关系并破坏认证能力。nonce 通常不需要保密,但它的唯一性或碰撞概率约束属于安全条件,不能把固定的全零 nonce 放在循环里。
Go 1.24 加入 cipher.NewGCMWithRandomNonce。它要求底层 cipher 来自 AES,内部生成随机 96 位 nonce,并把 nonce 放在密文前面。调用方传入的 nonce 参数是 nil;NonceSize() 返回 0,Overhead() 返回 28。官方限制同一密钥不能加密超过 2^32 条消息,用以约束随机碰撞风险;实际系统还可能需要更低的使用上限和其他数据量限制。Go 1.27.2 API
go
sealed := a.Seal(nil, nil, msg, aad)
plain, err := a.Open(nil, nil, sealed, aad)本例生成一次性随机 AES-256 密钥,加密后分别篡改密文和改动 AAD 中的租户,得到:
text
decrypt=true tamper-rejected=true aad-rejected=true nonce-size=0 overhead=28标签验证失败时必须丢弃结果,不能把失败后的缓冲区内容当明文继续处理。AAD 绑定能阻止把一条密文直接挪到不同上下文,但不会自动阻止在同一个上下文重放旧版本;需要单调版本、数据库状态或业务幂等规则进一步控制。
选择 AES-GCM 还是 ChaCha20-Poly1305,应看平台实现、硬件能力、依赖与合规要求,不凭算法名字推断吞吐。无论哪一个,随机源、密钥轮换、nonce 规则和错误处理都必须匹配具体 API。本文没有做性能基准,也没有测任何 nonce 碰撞概率。
HMAC 和签名:区别在验证者是否也能伪造
HMAC 把共享秘密与消息放进标准构造,产生认证码。发送方与接收方都知道同一个秘密,所以双方都能生成有效认证码。数字签名由私钥生成,公钥验证;把公钥发给十个验证服务,不会因此让它们都获得签名能力。RFC 2104、RFC 8032
| 决策维度 | HMAC-SHA-256 | Ed25519 等数字签名 |
|---|---|---|
| 验证方拿到什么 | 共享秘密 | 公钥 |
| 验证方是否能产生有效消息 | 能 | 仅有公钥时不能 |
| 常见用途 | 双方共享密钥的 webhook 或请求认证 | 多方验证制品、令牌或声明 |
| 密钥扩散后的影响 | 任意一处泄漏都可能允许伪造 | 公钥泄漏不等于私钥泄漏 |
| 仍然需要的协议设计 | 内容规范化、版本、受众、重放窗口 | 同样需要,并要可信分发公钥 |
不要用 SHA256(secret || message) 自创 MAC;使用 crypto/hmac。比较认证码使用 hmac.Equal,避免把普通字节比较引入的时序差异作为校验路径。签名则应使用成熟库,验证完整消息和正确的上下文,不能只验证一个由输入随意替换的摘要。Go hmac、Go ed25519
本机同时对原消息和修改后的消息验证,实际输出:
text
hmac-good=true hmac-changed=false signature-good=true signature-changed=false这些结果不提供保密,原消息仍然可以公开。签名也不会阻止原样重放;交易 ID、时间戳、用途和一次性状态必须进入协议。所谓“不可否认”还依赖私钥保管、身份绑定、审计和法律规则,不能由一个 Verify=true 单独推出。
TLS:验证证书只是建立安全通道的一部分
TLS 在传输中提供加密和完整性,并通过证书等机制认证通信方。TLS 1.3 的普通证书握手包含协商、密钥协商、证书与签名验证、Finished 验证,再用派生出的对称密钥保护应用数据。长期证书私钥与每次连接的流量密钥不是同一件东西。RFC 8446 §2
正在绘制图示…
查看图源代码
sequenceDiagram
participant C as 客户端
participant S as 服务端
C->>S: ClientHello 和密钥协商参数
S-->>C: ServerHello、可选 CertificateRequest、证书、握手签名、Finished
C->>C: 验证信任链、服务身份与握手完整性
opt mTLS 请求客户端证书
C->>S: 客户端证书与握手签名
S->>S: 验证客户端链与身份映射
end
C->>S: Finished
C<<->>S: 受保护的应用数据
这是省略了部分消息与恢复流程的概念图,不是抓包实测。TLS 保护的是两个 TLS 端点之间的链路;若流量在网关终止,网关到后端是否再次加密与认证要单独设计。它也不能修复服务端 SQL 注入或阻止持有合法会话的人越权。
证书链可信,主机名仍然可能不对
证书包含公钥、主体名称、有效期、用途等信息,并由发行者签名。通常服务端发送叶子证书和必要中间证书,客户端从这些材料构建到本地信任根的链。根证书的信任来自预先配置的信任库,而不是因为它“自己签了自己”。RFC 5280
| 检查 | 解决什么问题 | 不能替代什么 |
|---|---|---|
| 签名链到可信根 | 公钥和证书由受信任体系确认 | 目标主机名是否匹配 |
| SAN 中的服务身份 | 证书是否适用于所访问服务 | 用户有没有业务权限 |
| 有效期 | 是否在接受时间内 | 密钥是否已经泄漏 |
| Key Usage / EKU | 是否允许相应密码学或服务用途 | 完整的应用身份映射 |
| 吊销或短期凭据策略 | 如何停止接受不应继续使用的凭据 | 自动恢复已泄漏的数据 |
现代 TLS 服务身份验证应按 SAN 中的 DNS/IP 等标识处理,不依赖 Common Name 回退。RFC 9525 Go 的 ServerName 参与服务端身份验证及适用的 SNI;若只验证证书签名而不检查服务名,另一台同 CA 签发证书的服务也可能被误认。
实验生成一次性的测试 CA 与只允许 service.example、ServerAuth 的叶子证书,然后用 x509.Certificate.Verify 改变验证条件:
text
chain-host=true wrong-host=false wrong-usage=false missing-root=false四次使用同一张叶子证书。链和主机名都正确才通过;换服务名、要求客户端用途或不给可信根都失败。代码显式给空根池测试缺根,不依赖机器上安装了哪些系统根。这里验证的是证书链与选项,未执行 TLS 握手;Go 的 Certificate.Verify 文档还明确说明它不做吊销检查。固定版本 Verify
mTLS:客户端也用证书证明身份
普通 HTTPS 通常是客户端验证服务端,客户端用户身份再由会话或令牌处理。mTLS 额外让服务端请求并验证客户端证书。Go 服务端通常需要配置 ClientAuth: tls.RequireAndVerifyClientCert 和正确的 ClientCAs;仅设置 RequireAnyClientCert 只要求提供证书,不代表建立了可信链验证。客户端则仍需 RootCAs 和正确的服务身份验证。Go tls.Config
验证客户端证书后,还要把受信证书里的身份映射为工作负载或用户,并检查它能调用哪些动作。一个来自可信 CA 的客户端证书不应自动获得全部管理员权限。证书签发、短期更新、根轮换、吊销、会话恢复与连接池中的旧连接,都影响权限何时真正失效。
不要为了绕过证书错误把 InsecureSkipVerify 留在生产配置中。该选项会跳过标准的证书链与主机名验证,除非你另行正确实现验证,否则会失去防中间人的重要边界。优先选择当前维护的 TLS 默认实现,按兼容需求明确最低协议版本;在所有客户端支持时使用 TLS 1.3,保留 TLS 1.2 应有经过验证的兼容理由。TLS 推荐 BCP 195 / RFC 9325
最后回到密钥:算法正确,生命周期仍然可能出错
密码学系统需要知道密钥怎么生成、谁能使用、如何标识版本、旧数据如何解密,以及失窃后怎么停止使用。随机密钥用 crypto/rand,不要用 math/rand、时间戳或人类可记密码直接充当 AES/HMAC 密钥。测试中的临时随机密钥只存在进程内,没有真实账户和生产密钥。
密钥轮换应区分“新数据改用新密钥”和“旧密钥立即销毁”。旧密文若尚未重加密或重封装,删除旧密钥会造成不可恢复的数据丢失;反过来,只更新配置但无限期接受旧签名,也不算完成撤销。生产系统可用 KMS 或受控密钥服务降低密钥分发范围,但仍要限定调用权限并审计解密行为。
本文能复现算法接口的几个边界,不能替你完成密码系统设计。上线前最值得补的验证是目标硬件的 KDF 并发成本、密钥轮换故障恢复、TLS 双向握手与身份映射,以及真实数据备份能否在授权条件下恢复。
依据与验证边界
- 密码存储:OWASP Password Storage,查证日 2026-10-13;RFC 9106;
golang.org/x/crypto v0.58.0的 argon2 与 bcrypt 固定源码。参数建议与版本均有时效,本文没有宣称配置永远足够。 - 加密与认证:RFC 5116、NIST SP 800-38D、RFC 2104、RFC 8032;Go 1.27.2 的 cipher、hmac、ed25519、rand 文档。
- 传输和证书:RFC 8446、5280、9525、9325;Go 1.27.2 的 tls.Config 与 x509.Certificate.Verify。
- 本次 2026-10-13 在官方 Go 1.27.2、Linux/amd64 运行
go run .、go test -race -count=1 ./...、go vet ./...,全部通过。实际输出只报告布尔边界,不以单次时间当性能结论。 - 未运行真实 TLS/mTLS 握手、证书签发平台、吊销服务、KMS、生产密码迁移或抗拒绝服务压测。测试 CA 不加入操作系统信任库,密钥不写入磁盘。
完整复现代码
程序固定测试时间生成证书,每次生成新的临时密钥和盐。字符串 public demonstration password 是公开测试夹具,不是账号密码。固定参数用于复现;生产还需安全的记录格式、受限参数解析、限流与密钥生命周期管理。
go.mod
go.mod
module example.com/security04
go 1.27.2
require golang.org/x/crypto v0.58.0
require golang.org/x/sys v0.49.0 // indirectgo.sum
text
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=main.go
代码块 · 139 行 · 点击收起 / 展开
go
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/ed25519"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"fmt"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/bcrypt"
"math/big"
"time"
)
func random(n int) []byte {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return b
}
func derive(password, salt []byte) []byte { return argon2.IDKey(password, salt, 2, 19*1024, 1, 32) }
func passwordChecks() (bool, bool, bool, bool, error) {
password := []byte("public demonstration password")
salt := random(16)
stored := derive(password, salt)
good := subtle.ConstantTimeCompare(stored, derive(password, salt)) == 1
bad := subtle.ConstantTimeCompare(stored, derive([]byte("wrong"), salt)) == 1
different := !bytes.Equal(stored, derive(password, random(16)))
hash, err := bcrypt.GenerateFromPassword(password, 10)
if err != nil {
return false, false, false, false, err
}
bgood := bcrypt.CompareHashAndPassword(hash, password) == nil
return good, bad, different, bgood, nil
}
func aeadChecks() (bool, bool, bool, int, int, error) {
block, err := aes.NewCipher(random(32))
if err != nil {
return false, false, false, 0, 0, err
}
a, err := cipher.NewGCMWithRandomNonce(block)
if err != nil {
return false, false, false, 0, 0, err
}
msg := []byte("public fixture")
aad := []byte("tenant=a;record=7;version=1")
sealed := a.Seal(nil, nil, msg, aad)
plain, err := a.Open(nil, nil, sealed, aad)
good := err == nil && bytes.Equal(msg, plain)
tampered := bytes.Clone(sealed)
tampered[len(tampered)-1] ^= 1
_, bad := a.Open(nil, nil, tampered, aad)
_, wrongAAD := a.Open(nil, nil, sealed, []byte("tenant=b;record=7;version=1"))
return good, bad != nil, wrongAAD != nil, a.NonceSize(), a.Overhead(), nil
}
func macAndSignature() (bool, bool, bool, bool) {
key := random(32)
msg := []byte("public invoice fixture")
mac := func(b []byte) []byte { h := hmac.New(sha256.New, key); h.Write(b); return h.Sum(nil) }
tag := mac(msg)
mgood := hmac.Equal(tag, mac(msg))
mbad := hmac.Equal(tag, mac([]byte("changed")))
pub, priv, err := ed25519.GenerateKey(rand.Reader)
must(err)
sig := ed25519.Sign(priv, msg)
return mgood, mbad, ed25519.Verify(pub, msg, sig), ed25519.Verify(pub, []byte("changed"), sig)
}
type certificates struct {
ca, leaf *x509.Certificate
roots *x509.CertPool
}
func makeCertificates() (certificates, error) {
var out certificates
pub, caKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return out, err
}
now := time.Date(2026, 10, 13, 12, 0, 0, 0, time.UTC)
ca := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "PUBLIC TEST CA"}, NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour), IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign}
der, err := x509.CreateCertificate(rand.Reader, ca, ca, pub, caKey)
if err != nil {
return out, err
}
ca, err = x509.ParseCertificate(der)
if err != nil {
return out, err
}
leafPub, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return out, err
}
leaf := &x509.Certificate{SerialNumber: big.NewInt(2), DNSNames: []string{"service.example"}, NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}}
der, err = x509.CreateCertificate(rand.Reader, leaf, ca, leafPub, caKey)
if err != nil {
return out, err
}
leaf, err = x509.ParseCertificate(der)
if err != nil {
return out, err
}
roots := x509.NewCertPool()
roots.AddCert(ca)
return certificates{ca, leaf, roots}, nil
}
func verifyCert(c certificates, name string, usage x509.ExtKeyUsage, roots *x509.CertPool) bool {
_, err := c.leaf.Verify(x509.VerifyOptions{Roots: roots, DNSName: name, CurrentTime: time.Date(2026, 10, 13, 12, 0, 0, 0, time.UTC), KeyUsages: []x509.ExtKeyUsage{usage}})
return err == nil
}
func main() {
fmt.Println("base64:", base64.StdEncoding.EncodeToString([]byte("hello")))
good, bad, salts, bgood, err := passwordChecks()
must(err)
fmt.Printf("argon-good=%v argon-wrong=%v different-salts=%v bcrypt-good=%v\n", good, bad, salts, bgood)
_, err = bcrypt.GenerateFromPassword(bytes.Repeat([]byte("x"), 73), 10)
fmt.Println("bcrypt-73-bytes-rejected:", err == bcrypt.ErrPasswordTooLong)
g, t, a, n, o, err := aeadChecks()
must(err)
fmt.Printf("decrypt=%v tamper-rejected=%v aad-rejected=%v nonce-size=%d overhead=%d\n", g, t, a, n, o)
mg, mb, sg, sb := macAndSignature()
fmt.Printf("hmac-good=%v hmac-changed=%v signature-good=%v signature-changed=%v\n", mg, mb, sg, sb)
c, err := makeCertificates()
must(err)
fmt.Printf("chain-host=%v wrong-host=%v wrong-usage=%v missing-root=%v\n", verifyCert(c, "service.example", x509.ExtKeyUsageServerAuth, c.roots), verifyCert(c, "other.example", x509.ExtKeyUsageServerAuth, c.roots), verifyCert(c, "service.example", x509.ExtKeyUsageClientAuth, c.roots), verifyCert(c, "service.example", x509.ExtKeyUsageServerAuth, x509.NewCertPool()))
}
func must(err error) {
if err != nil {
panic(err)
}
}main_test.go
代码块 · 39 行 · 点击收起 / 展开
go
package main
import (
"bytes"
"crypto/x509"
"golang.org/x/crypto/bcrypt"
"testing"
)
func TestPassword(t *testing.T) {
good, bad, salts, bgood, err := passwordChecks()
if err != nil || !good || bad || !salts || !bgood {
t.Fatal(good, bad, salts, bgood, err)
}
if _, err := bcrypt.GenerateFromPassword(bytes.Repeat([]byte("x"), 73), 10); err != bcrypt.ErrPasswordTooLong {
t.Fatal(err)
}
}
func TestAEAD(t *testing.T) {
good, tamper, aad, n, o, err := aeadChecks()
if err != nil || !good || !tamper || !aad || n != 0 || o != 28 {
t.Fatal(good, tamper, aad, n, o, err)
}
}
func TestAuthenticity(t *testing.T) {
mg, mb, sg, sb := macAndSignature()
if !mg || mb || !sg || sb {
t.Fatal(mg, mb, sg, sb)
}
}
func TestCertificate(t *testing.T) {
c, err := makeCertificates()
if err != nil {
t.Fatal(err)
}
if !verifyCert(c, "service.example", x509.ExtKeyUsageServerAuth, c.roots) || verifyCert(c, "wrong.example", x509.ExtKeyUsageServerAuth, c.roots) || verifyCert(c, "service.example", x509.ExtKeyUsageClientAuth, c.roots) || verifyCert(c, "service.example", x509.ExtKeyUsageServerAuth, x509.NewCertPool()) {
t.Fatal("certificate boundary")
}
}运行命令
sh
go mod download
go run .
go test -race -count=1 ./...
go vet ./...