Python 基础体系 · 第 61/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。
Python 哈希、HMAC 与 TLS:完整性、密码存储、证书和误区
在 Python 安全编程中,hashlib、hmac、secrets 和 ssl 经常同时出现,但它们解决的是不同问题:
- 哈希:把任意长度输入映射为固定长度摘要,主要用于完整性校验、内容寻址和指纹。
- HMAC:使用共享密钥计算消息认证码,解决“消息是否被篡改,以及是否由掌握密钥的一方生成”。
- 密码存储:不是简单调用哈希,而是使用带盐、可调成本的密码派生函数。
- TLS:在网络连接层提供加密、完整性和对端认证;证书用于认证身份,不等于“证书本身加密了数据”。
secrets:从操作系统获得适合安全用途的随机数,用来生成盐、令牌和密钥,而不是使用面向模拟的random。
这些概念的共同底层是密码学,但安全目标、密钥状态、攻击模型和生命周期都不同。把一个工具替代另一个工具,通常会导致“代码看起来用了密码学,实际却没有获得相应安全性”。
一、先区分四种安全目标
1. 完整性:数据有没有被修改
完整性关注的是:
接收方能否发现消息内容在传输或存储过程中发生了变化?
例如,下载文件后重新计算 SHA-256 摘要:
from pathlib import Path
import hashlib
path = Path("example.bin")
digest = hashlib.file_digest(path.open("rb"), "sha256").hexdigest()
print(digest)
hashlib.file_digest() 会读取文件并返回摘要对象。Python 3.14 中,如果文件以非阻塞模式打开,可能抛出 BlockingIOError;调用后文件对象也应被视为处于未知状态,因此示例中由 open() 创建的文件对象应及时关闭。(docs.python.org)
更稳妥的写法是显式管理文件生命周期:
import hashlib
with open("example.bin", "rb") as f:
digest = hashlib.file_digest(f, "sha256").hexdigest()
print(digest)
但是,普通哈希只能回答:
当前计算出的摘要是否等于某个已知摘要?
它不能回答:
这个已知摘要是不是攻击者自己伪造的?
攻击者可以同时替换文件和摘要:
原始文件 -> example.bin
原始摘要 -> example.bin.sha256
攻击者替换为:
恶意文件 -> example.bin
恶意文件摘要 -> example.bin.sha256
因此,普通哈希适合检测偶然损坏、文件去重、缓存键和内容指纹,但不能单独抵抗主动攻击者。
2. 认证:消息是否来自掌握密钥的一方
认证关注的是:
消息不仅没有被修改,而且是否由拥有某个秘密的实体生成?
这需要密钥。HMAC 就是这种机制。
3. 机密性:窃听者能不能看懂内容
机密性需要加密。哈希和 HMAC 都不是加密:
- 哈希没有解密操作;
- HMAC 没有解密操作;
- 两者都不隐藏原始消息。
TLS 则同时使用加密和认证机制,保护网络连接中的数据。
4. 身份认证:对方到底是谁
即使连接被加密,也还需要确认:
我连接的服务器真的是目标服务器,而不是中间人?
TLS 通过证书链、主机名校验和受信任 CA 来解决这个问题。证书是身份绑定材料,不是简单的“加密文件”。
二、哈希函数:输入、摘要和安全性质
1. 哈希的形式化模型
设哈希函数为:
其中:
- :任意长度的比特串;
- :长度固定为 位的摘要空间;
- :消息 的摘要。
以 SHA-256 为例,输入可以是任意长度,但输出始终是 256 位,也就是 32 字节,十六进制表示通常是 64 个字符。
import hashlib
data = b"hello"
raw = hashlib.sha256(data).digest()
text = hashlib.sha256(data).hexdigest()
print(len(raw)) # 32
print(len(text)) # 64
print(text)
digest() 返回原始二进制摘要;hexdigest() 只是把每个字节转换成两个十六进制字符,便于日志、文本协议和命令行传输。十六进制字符串并没有比原始摘要“更安全”,只是表示形式不同。
2. 哈希对象是增量状态机
哈希计算可以分块进行:
import hashlib
h1 = hashlib.sha256()
h1.update(b"hello")
h1.update(b" ")
h1.update(b"world")
h2 = hashlib.sha256(b"hello world")
assert h1.digest() == h2.digest()
哈希对象内部维护一个状态。连续调用:
h.update(a)
h.update(b)
等价于对拼接结果计算:
hash(a + b)
这里的“拼接”是字节拼接,不是字符串语义拼接。以下两个输入不同:
b"ab" + b"c" # b"abc"
b"a" + b"bc" # b"abc"
而下面两个逻辑对象可能产生同样的序列化结果:
["ab", "c"]
["a", "bc"]
如果业务需要对结构化数据做签名或认证,就必须定义明确的序列化规则,例如长度前缀、固定字段顺序或规范化 JSON。不能直接依赖“看起来一样”的对象表示。
3. 哈希的三个主要安全性质
原像抗性
已知摘要 ,难以找到消息 ,使得:
这对应“从摘要反推输入”。
第二原像抗性
已知消息 ,难以找到不同消息 ,使得:
碰撞抗性
难以找到任意两个不同消息 ,满足:
普通哈希函数的输出空间有限,而输入空间无限,因此数学上必然存在碰撞。密码学要求的是:攻击者在实际计算资源内难以找到有用碰撞。
Python 文档明确指出,MD5 和 SHA-1 存在已知碰撞弱点;安全场景通常应选择 SHA-2、SHA-3 或 BLAKE2 等现代算法。hashlib 中的具体可用算法还可能受 OpenSSL 构建影响。(docs.python.org)
4. usedforsecurity 不会让弱算法变强
部分哈希构造器支持:
import hashlib
digest = hashlib.md5(b"legacy data", usedforsecurity=False).hexdigest()
usedforsecurity=False 的用途是告诉受限环境:
这个哈希只用于非安全用途,例如兼容旧文件格式或非密码学校验。
它不会修复 MD5 或 SHA-1 的碰撞问题,也不应该被用来绕过安全策略后继续执行密码学认证。
三、HMAC:带共享密钥的消息认证
1. HMAC 解决什么问题
HMAC 的基本形式是:
其中:
- :底层哈希函数;
- :秘密密钥;
- :经过长度调整后的密钥;
- :消息;
- 、:固定的内外填充值;
- :拼接;
- :按位异或。
直觉上,HMAC 对消息做了两层带密钥的哈希:
- 内层使用密钥和消息计算摘要;
- 外层再次使用密钥包裹内层摘要。
接收方只有在知道同一个密钥 时,才能计算出相同的认证码。
import hashlib
import hmac
key = b"server-client-shared-secret"
message = b"amount=100¤cy=CNY"
tag = hmac.digest(key, message, "sha256")
print(tag.hex())
received_tag = tag
assert hmac.compare_digest(tag, received_tag)
hmac.digest() 适合消息已经在内存中的场景;对于流式数据,应使用 HMAC 对象持续调用 update()。该模块实现 RFC 2104,并要求底层摘要具有固定长度,因此 SHAKE-128 和 SHAKE-256 这类可变输出哈希不能用于 HMAC。(docs.python.org)
2. 为什么不能使用普通哈希模拟 HMAC
下面这种写法不安全:
hashlib.sha256(key + message).digest()
它的问题不在于“少写了几行代码”,而在于普通哈希的消息组合方式没有经过 HMAC 的结构化设计。对于某些基于 Merkle–Damgård 构造的哈希函数,简单拼接还会涉及长度扩展攻击等问题。
HMAC 的设计目标就是把底层哈希函数放进经过分析的密钥结构中。工程代码应直接使用 hmac.new() 或 hmac.digest(),而不是自定义“加密哈希”。
3. 验证时使用 compare_digest
错误示例:
if expected_tag == received_tag:
accept()
普通 == 可能在发现第一个不同字节时提前返回。远程攻击者如果能够重复测量响应时间,理论上可能利用这种差异推断秘密值的前缀。
正确写法:
if not hmac.compare_digest(expected_tag, received_tag):
raise ValueError("invalid message authentication code")
compare_digest() 要求两边都是同类对象:都为 ASCII 字符串,或都为 bytes-like 对象。长度不同和类型错误仍可能暴露长度或类型信息,但不会直接暴露比较值本身。Python 3.10 及之后,在可用时会使用 OpenSSL 的 CRYPTO_memcmp()。(docs.python.org)
4. HMAC 不提供防重放
假设客户端发送:
message = transfer=100
tag = HMAC(key, message)
攻击者虽然不能修改 transfer=100 而保持合法标签,但可以完整复制这对数据,再发送一次。服务器仍会验证通过。
因此,防重放需要把以下信息纳入认证范围:
- 单调递增序列号;
- 时间戳和有效期;
- 一次性随机数;
- 请求 ID,并在服务端记录已使用的 ID。
例如:
import hashlib
import hmac
key = b"shared-key"
request_id = b"9b1deb4d-7b7e-4b2f-a6b3-3f0e4d5a2b11"
body = b"amount=100"
signed = request_id + b"\x00" + body
tag = hmac.digest(key, signed, hashlib.sha256)
这里的 \x00 是明确的字段分隔符。生产协议应进一步定义字段编码,避免不同字段组合后产生歧义。
四、密码存储:哈希、HMAC 和密码派生不能混为一谈
1. 密码不是普通数据
数据库密码泄露后,攻击者通常会离线尝试大量候选密码:
guess_1 -> 计算 -> 与数据库值比较
guess_2 -> 计算 -> 与数据库值比较
guess_3 -> 计算 -> 与数据库值比较
因此,密码存储算法的目标不是“让服务器快速验证”,而是:
即使数据库泄露,也让攻击者验证每个候选密码尽可能昂贵。
普通 SHA-256 的优点——快速——在密码存储中反而是缺点。
错误示例:
import hashlib
stored = hashlib.sha256(password.encode()).hexdigest()
即使加盐,单次 SHA-256 仍然太快:
stored = hashlib.sha256(salt + password.encode()).digest()
加盐可以阻止预计算彩虹表和相同密码产生相同数据库值,但不能把快速哈希变成适合密码存储的慢算法。
2. 盐、密钥和密码的区别
盐
盐是每个账户独立生成的随机公开值:
salt = random bytes
stored = KDF(password, salt, cost)
盐不需要保密,可以和派生结果一起存储。它的作用是让相同密码在不同账户上产生不同结果,并阻止攻击者复用预计算结果。
密钥
密钥用于 HMAC 或加密,必须保密。密钥泄露通常意味着攻击者可以伪造认证码或解密数据。
密码
密码通常由人选择,熵低且可能重复使用。因此需要密码派生函数进行“加盐”和“拉伸”。
3. 使用 scrypt
Python 3.14 的 hashlib 提供 scrypt()。它同时增加 CPU 和内存成本,参数分别为:
n:CPU/内存成本因子,通常要求为 2 的幂;r:块大小;p:并行化参数;maxmem:内存上限;dklen:派生结果长度。
import base64
import hashlib
import secrets
def hash_password(password: str) -> str:
if not isinstance(password, str):
raise TypeError("password must be str")
password_bytes = password.encode("utf-8")
salt = secrets.token_bytes(16)
derived = hashlib.scrypt(
password_bytes,
salt=salt,
n=2**14,
r=8,
p=1,
dklen=32,
)
# 格式中保存算法和参数,便于未来升级
return (
"scrypt$"
"n=16384,r=8,p=1$"
f"{base64.b64encode(salt).decode('ascii')}$"
f"{base64.b64encode(derived).decode('ascii')}"
)
def verify_password(password: str, encoded: str) -> bool:
try:
algorithm, parameters, salt_text, derived_text = encoded.split("$")
if algorithm != "scrypt":
return False
values = dict(item.split("=") for item in parameters.split(","))
n = int(values["n"])
r = int(values["r"])
p = int(values["p"])
salt = base64.b64decode(salt_text, validate=True)
expected = base64.b64decode(derived_text, validate=True)
actual = hashlib.scrypt(
password.encode("utf-8"),
salt=salt,
n=n,
r=r,
p=p,
dklen=len(expected),
)
return hmac.compare_digest(actual, expected)
except (ValueError, KeyError, TypeError):
return False
调用:
record = hash_password("correct horse battery staple")
print(record)
print(verify_password("correct horse battery staple", record)) # True
print(verify_password("wrong password", record)) # False
hashlib.scrypt() 的 salt 应来自合适的随机源,Python 文档建议盐至少约 16 字节;n、r、p 和 maxmem 会影响 CPU、内存和并发容量,不能直接照搬示例参数到所有生产环境。应在目标机器上测量单次登录验证成本,并设置合理的登录限速。(docs.python.org)
示例保存了参数,是因为未来可能需要提高成本。如果只保存一个没有算法和参数信息的摘要,升级策略会变得困难。
4. 使用 PBKDF2-HMAC
hashlib.pbkdf2_hmac() 使用 HMAC 作为伪随机函数,适用于需要 PBKDF2 的协议或兼容已有系统的场景:
import hashlib
import hmac
import secrets
password = b"correct horse battery staple"
salt = secrets.token_bytes(16)
iterations = 500_000
derived = hashlib.pbkdf2_hmac(
"sha256",
password,
salt,
iterations,
dklen=32,
)
candidate = hashlib.pbkdf2_hmac(
"sha256",
password,
salt,
iterations,
dklen=32,
)
assert hmac.compare_digest(derived, candidate)
这里的关键关系是:
密码
+ 唯一随机盐
+ 足够高且可升级的迭代次数
-> PBKDF2-HMAC
-> 存储派生结果
Python 文档将 pbkdf2_hmac() 和 scrypt()归入密码密钥派生用途,并明确说明 sha1(password) 这样的简单算法无法抵抗暴力破解。PBKDF2 的迭代次数必须根据实际硬件和服务负载测量,而不是把示例中的数值当成永久标准。(docs.python.org)
5. 为什么 HMAC 不是密码存储方案
如果服务端直接保存:
hmac.digest(server_secret, password.encode(), "sha256")
这属于“使用固定秘密对密码做认证码”,仍然不适合作为一般密码存储方案:
- 所有用户可能共享同一个服务器密钥;
- 密钥一旦泄露,所有密码记录的保护方式同时失效;
- HMAC 的设计目标是消息认证,不是专门抵抗离线密码猜测;
- 服务器验证密码时仍缺乏独立的、可调节的密码工作因子。
如果业务需求是验证 API 密钥,通常可以把 API 密钥当作高熵随机令牌,并保存其哈希或派生结果;如果是人类密码,则应使用专门的密码哈希方案。
五、secrets:随机性是密码学系统的输入
1. random 和 secrets 的边界
random 面向模拟、测试和一般程序逻辑。它的伪随机序列如果种子和状态被推断,后续输出可能被预测。
secrets 使用操作系统提供的安全随机源,适用于密码、认证令牌和其他秘密数据。官方文档明确建议,在安全场景中优先使用 secrets,而不是面向建模和模拟的 random。(docs.python.org)
import secrets
salt = secrets.token_bytes(16)
reset_token = secrets.token_urlsafe(32)
api_key = secrets.token_hex(32)
print(len(salt)) # 16
print(len(api_key)) # 64 个十六进制字符
print(reset_token)
token_hex(32) 表示 32 字节随机数据,编码后通常是 64 个十六进制字符;token_urlsafe(32) 使用 URL 安全的 Base64 表示,文本长度不是固定的 64。文档将 32 字节随机性作为典型场景的参考量,但令牌有效期、撤销机制、泄露渠道和访问频率同样重要。(docs.python.org)
2. 随机盐和随机令牌不是一回事
盐可以公开保存,令牌通常必须保密:
密码盐:
可以放入数据库
只要求不可预测、每个账户独立
密码重置令牌:
发送给用户
需要不可预测、短时有效、使用后失效
HMAC 密钥:
不应出现在数据库普通字段、日志或响应中
用于持续认证
不要因为它们都由 secrets.token_bytes() 生成,就认为它们具有相同的生命周期和存储策略。
六、TLS:网络连接中的加密、完整性和身份认证
1. TLS 的组件
一个典型 TLS 客户端连接包含:
sequenceDiagram
participant C as Python 客户端
participant S as 服务器
participant CA as 受信任 CA 集合
C->>S: TCP 连接
C->>S: ClientHello(支持的 TLS 版本、密码套件等)
S->>C: ServerHello + 证书链
C->>CA: 验证证书链
C->>C: 校验主机名、有效期和用途
C->>S: 完成密钥协商
C->>S: 加密的应用数据
S->>C: 加密的应用数据
TLS 握手大致完成三件事:
- 协商协议版本和密码套件;
- 通过证书认证服务器身份;
- 协商会话密钥,随后使用对称加密保护应用数据。
证书本身通常包含公钥、主体名称、有效期、签发者和扩展字段。服务器使用与证书公钥对应的私钥证明自己拥有该身份。真正大量加密应用数据时,通常使用握手阶段协商出的对称会话密钥,因为对称加密更适合高吞吐数据传输。
2. Python 中创建正确的客户端 TLS 上下文
import socket
import ssl
hostname = "www.python.org"
port = 443
context = ssl.create_default_context()
with socket.create_connection((hostname, port), timeout=10) as raw_socket:
with context.wrap_socket(raw_socket, server_hostname=hostname) as tls_socket:
print(tls_socket.version())
print(tls_socket.cipher())
print(tls_socket.getpeercert()["subject"])
request = (
f"GET / HTTP/1.1\r\n"
f"Host: {hostname}\r\n"
f"Connection: close\r\n"
f"\r\n"
).encode("ascii")
tls_socket.sendall(request)
response = b""
while True:
chunk = tls_socket.recv(4096)
if not chunk:
break
response += chunk
print(response[:200])
关键点不是 wrap_socket() 这一个调用,而是三个参数和状态:
ssl.create_default_context()创建面向常见用途的安全上下文;server_hostname=hostname提供主机名,用于 SNI 和主机名校验;- 连接只有在 TLS 握手和证书校验成功后,才进入应用数据阶段。
create_default_context() 会根据用途设置较高安全等级;以服务器认证为目的时,会启用证书验证并加载指定或系统默认的 CA 证书。(docs.python.org)
3. 不要关闭证书验证来“解决问题”
危险代码:
context = ssl._create_unverified_context()
或者:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
这样做可能让连接“成功”,但攻击者可以伪造服务器,读取或修改通信内容。常见失败表现是:
ssl.SSLCertVerificationError
ssl.SSLError
正确的诊断顺序应是:
- 确认访问的主机名与证书中的 SAN 是否匹配;
- 确认系统时间正确;
- 确认客户端拥有正确的 CA 根证书;
- 检查证书是否过期、尚未生效或链不完整;
- 检查代理、企业 TLS 检查设备和本地信任库;
- 只有在确认信任模型后,才决定是否使用自定义 CA。
如果是内部服务,应向上下文加载内部 CA,而不是关闭验证:
import ssl
context = ssl.create_default_context(
purpose=ssl.Purpose.SERVER_AUTH,
cafile="/etc/my-company/internal-ca.pem",
)
4. 服务器端 TLS 上下文
服务器至少需要证书链和私钥:
import socket
import ssl
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(
certfile="server-chain.pem",
keyfile="server-key.pem",
)
with socket.socket() as listener:
listener.bind(("127.0.0.1", 8443))
listener.listen(5)
with context.wrap_socket(listener, server_side=True) as tls_listener:
tls_socket, address = tls_listener.accept()
with tls_socket:
data = tls_socket.recv(4096)
tls_socket.sendall(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK")
服务器证书文件往往需要包含服务器证书及中间证书链;私钥必须严格保护。监听套接字被包装后,接受到的连接会执行 TLS 握手。握手失败时,不能把连接当作普通明文连接继续处理。
在真实服务中,还需要处理:
- 握手失败;
- 客户端提前断开;
- 超时;
- 半关闭连接;
- 证书轮换;
- 多线程、异步事件循环或进程模型中的上下文复用。
SSLContext 通常应在进程启动阶段配置好并复用,而不是每个请求临时创建。具体协议能力还依赖 CPython 构建时使用的 OpenSSL 和平台套接字行为。(docs.python.org)
七、TLS 完整性和应用层 HMAC 不是重复功能
TLS 已经为连接中的应用数据提供完整性保护,因此在一个正常验证的 HTTPS 连接内,再给每个请求增加 HMAC 通常不是为了“让 TLS 更安全”。
两者的信任边界不同:
TLS:
保护客户端 <-> TLS 终止点
终止点可能是反向代理、网关或负载均衡器
应用层 HMAC:
可继续保护服务 <-> 服务之间的消息
可跨越代理、队列、缓存和持久化边界
例如:
- 浏览器到 API 网关:TLS 足够保护传输;
- API 网关把消息放入消息队列:可以对消息做应用层签名或 HMAC;
- 文件长期存储:可以使用签名或带密钥的完整性校验;
- 多个不完全互信的服务:应用层认证可以明确消息来源。
TLS 是连接状态协议;HMAC 是消息级原语。TLS 连接结束后,单独保存的 HMAC 仍可用于验证消息,而 TLS 会话密钥通常不会承担这种长期消息认证职责。
八、证书不是公钥指纹,也不是服务器密码
1. 证书链的验证逻辑
客户端收到服务器证书后,大致验证:
- 证书是否由受信任 CA 签发;
- 签名链能否连接到本地信任根;
- 当前时间是否位于有效期内;
- 证书用途是否允许服务器认证;
- 请求主机名是否匹配证书的 SAN;
- 证书是否被撤销或受到其他策略限制。
其中“证书签名有效”只说明:
某个受信任签发者为这份证书背书。
它不自动说明:
当前连接的主机名一定与证书匹配。
所以 server_hostname 很重要。只验证证书链、不验证主机名,仍可能遭遇中间人攻击。
2. 证书固定与自定义 CA
证书固定是把客户端信任范围进一步收窄,例如固定某个公钥或证书指纹。它可能降低错误 CA 签发证书带来的风险,但也会增加证书轮换、备份证书、应急恢复和代理环境兼容成本。
内部系统更常见的方案是:
- 建立明确的内部 CA;
- 将内部根 CA 安装到客户端信任库;
- 使用正常的主机名校验;
- 管理证书轮换和撤销。
不要把“固定当前叶子证书”当成默认安全升级。它是一种特殊信任模型,必须配套轮换和恢复流程。
九、常见误区与实际失败表现
误区一:Base64 可以加密
import base64
encoded = base64.b64encode(b"secret")
decoded = base64.b64decode(encoded)
Base64 只是编码。任何看到数据的人都能解码。它适合解决二进制数据进入文本协议的问题,不提供机密性、完整性或认证。
误区二:哈希可以解密
哈希通常是单向函数。密码验证不是“解密数据库中的密码”,而是:
输入候选密码
-> 使用相同盐和参数重新派生
-> 与数据库中的派生结果比较
因此服务端不应该需要还原用户原始密码。
误区三:盐必须保密
盐的主要作用是独立化每次密码派生,不是作为密钥。盐可以存入数据库;真正必须保密的是 HMAC 密钥、加密密钥和服务端秘密。
误区四:只要用了 HTTPS,就不用验证证书
如果客户端关闭 CERT_REQUIRED 或主机名校验,HTTPS 可能退化为“加密但不认证”的连接。攻击者仍可建立一个自己的 TLS 服务器,让客户端与攻击者完成加密握手。
误区五:TLS 可以防止所有应用层问题
TLS 保护的是连接中的字节流,不能自动防止:
- SQL 注入;
- 命令注入;
- 不安全反序列化;
- 越权;
- 错误的业务授权;
- 日志泄露 Secret;
- 服务端将恶意输入传给其他系统。
TLS 不能替代输入验证、参数化查询、最小权限和安全依赖管理。
误区六:HMAC 密钥可以放在请求里
如果请求包含:
message
key
HMAC(key, message)
接收方和攻击者都知道密钥,攻击者可以自行修改消息并重新计算 HMAC。HMAC 的前提是验证方和攻击方不能同时获得密钥。
误区七:只比较十六进制字符串就足够
应确保:
- 编码规则固定;
- 大小写规则固定;
- 没有隐式 Unicode 规范化差异;
- 比较使用
compare_digest(); - 长度和格式在比较前经过验证。
例如,"é".encode("utf-8") 与某些其他编码的结果不同。密码和消息必须明确规定编码,不能依赖平台默认编码。
十、一个最小的选择模型
可以按安全目标选择工具:
| 需求 | 合适工具 | 是否需要秘密 |
|---|---|---|
| 文件指纹、缓存键 | hashlib.sha256()、BLAKE2 |
否 |
| 检查偶然传输损坏 | 哈希或校验和 | 否 |
| 防止有密钥的攻击者篡改消息 | hmac 或 BLAKE2 keyed mode |
是 |
| 保存人类密码 | hashlib.scrypt() 或 pbkdf2_hmac() |
否,但需要随机盐 |
| 生成密码重置令牌 | secrets.token_urlsafe() |
令牌本身是秘密 |
| 客户端到服务器的传输保护 | ssl.create_default_context() |
由 TLS 管理 |
| 服务器身份认证 | TLS 证书链和主机名校验 | 私钥必须保密 |
BLAKE2 也支持 keyed mode,可以作为 HMAC 的一种更直接替代,但这不意味着它可以用于密码存储。Python 文档特别提醒,普通哈希或 BLAKE2 的 salted hashing 仍不适合密码哈希。(docs.python.org)
十一、最终检查:每个原语保护的边界
判断一段安全代码是否合理,可以连续追问四个问题:
-
攻击者能修改什么?
如果只能造成随机损坏,普通哈希可能足够;如果攻击者可以主动伪造,就需要认证密钥或数字签名。 -
攻击者能看到什么?
如果消息内容必须保密,需要加密;哈希和 HMAC 都不会隐藏消息。 -
验证方如何知道密钥或信任根?
HMAC 依赖预共享秘密;TLS 依赖证书链、CA 和主机名策略。 -
秘密泄露后的影响是什么?
密码数据库泄露、HMAC 密钥泄露、TLS 私钥泄露和重置令牌泄露,恢复方式完全不同。
可以把完整流程概括为:
安全随机数
-> 生成盐、令牌或密钥
哈希
-> 内容指纹和非密钥完整性
密码派生函数
-> 慢速、带盐地存储人类密码
HMAC
-> 使用共享密钥认证消息
TLS
-> 在网络连接中协商密钥、验证身份并保护数据
最重要的不是记住某个 API,而是先明确安全目标,再选择满足该目标的原语。sha256(password)、HMAC(password)、Base64、关闭证书校验,看起来都可能“让程序跑起来”,但它们并没有替代密码存储、身份认证或机密性保护。
系列导航与关联阅读
- 系列入口:Python 完整学习路线:从语言模型、并发到 Web、数据、AI 与生产交付
- 上一篇:Python HTTP 客户端:连接池、超时、重试、流式和 TLS
- 下一篇:Python pyproject.toml:构建系统、项目元数据、入口和工具配置
- 延伸:Python 随机数与 secrets:伪随机、采样、种子和密码学边界
- 延伸:Python 安全工程:输入、注入、反序列化、依赖、Secret 和沙箱
官方资料
本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论