功能说明
使用普通等号比较令牌时,语言实现通常会在第一个不同字节处提前返回,攻击者可以通过大量请求测量响应时间,逐字节猜出正确前缀。虽然网络抖动会干扰测量,但在内网或高并发条件下仍可能被统计出来。安全比较必须在整个长度上花费相同时间。
直接用 subtle.ConstantTimeCompare 比较两个字符串时,长度不同会立即返回零,仍然泄漏长度。本例先对两侧做 SHA-256,得到固定三十二字节摘要,再比较摘要。这样无论输入长度如何,比较阶段的工作量一致,长度信息只影响哈希阶段,而哈希耗时与长度的关系远弱于逐字节比较,且无法帮助恢复内容。
关键代码
ConstantTimeEqual 分别计算两侧的 SHA-256,返回定长数组,切片后交给 subtle.ConstantTimeCompare。该函数对等长切片逐字节异或并累积结果,不提前退出,最终只有完全相等才返回一。main 演示正确值、末位错误的同长度值和长度不同的短值,三种情况分别返回真、假、假。
使用注意
哈希后再比较并不等于对整个流程做常量时间处理,输入长度仍会影响哈希耗时;对极敏感场景应先把令牌解码为定长字节再比较。不要用该函数比较密码明文,密码应使用 Argon2 或 bcrypt 的验证接口。比较结果不要直接写入日志细节,避免旁路泄漏。该函数只解决比较环节,令牌的生成、存储、传输仍需使用加密随机数、哈希存储和 TLS。哈希碰撞概率极低但不为零,安全设计不能依赖碰撞假设。
验证方式
运行脚本,前三行依次输出 true、false、false,说明只有完全一致才通过。把最后一个用例改为相同长度的错误值,输出仍为 false,且耗时与正确比较接近。要观察时序差异,需要在受控环境中进行大量重复测量,普通功能测试无法证明常量时间。
代码示例
package main
import (
"crypto/sha256"
"crypto/subtle"
"fmt"
)
func ConstantTimeEqual(a, b string) bool {
left := sha256.Sum256([]byte(a))
right := sha256.Sum256([]byte(b))
return subtle.ConstantTimeCompare(left[:], right[:]) == 1
}
func main() {
secret := "s3cr3t-token"
fmt.Println(ConstantTimeEqual(secret, "s3cr3t-token"))
fmt.Println(ConstantTimeEqual(secret, "s3cr3t-tokeX"))
fmt.Println(ConstantTimeEqual(secret, "short"))
}