← 中文资源

简体中文版本

常量时间比较密钥

先对两侧字符串做 SHA-256 得到定长摘要,再用常量时间比较,避免长度与前缀信息通过响应时间泄漏。

阅读英文原文

功能说明

使用普通等号比较令牌时,语言实现通常会在第一个不同字节处提前返回,攻击者可以通过大量请求测量响应时间,逐字节猜出正确前缀。虽然网络抖动会干扰测量,但在内网或高并发条件下仍可能被统计出来。安全比较必须在整个长度上花费相同时间。

直接用 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"))
}