2. 密码学基础与工程接口

在密码库集成中,最容易出错的地方往往不是“不知道 AES 是什么”,而是不知道一个密码学概念在 API 中究竟对应哪些函数、哪些参数、哪些返回值,以及这些参数之间有什么隐含约束。同一个“加密”概念,在 OpenSSL、libsodium、Bouncy Castle、WebCrypto 中的接口形态差异很大;如果只按数学定义去调用,很容易出现 IV 复用、密钥长度不匹配、签名与验签参数顺序颠倒、把哈希当 MAC 用等问题。本节把常见密码学概念与密码库 API 的映射关系梳理清楚,重点说明调用语义和工程约束。

一、概念到 API 的映射总览

密码学原语在工程接口中通常被拆成“算法标识 + 密钥材料 + 参数 + 输入数据 + 输出缓冲”五类要素。以对称加密为例,概念上的“用密钥加密明文”在 API 中往往表现为:选择算法套件(如 AES-256-GCM)、传入密钥、传入随机生成的 IV/Nonce、传入明文、接收密文与认证标签。非对称加密则通常拆成密钥生成、公钥加密/私钥解密、私钥签名/公钥验签三组接口。哈希是单向压缩函数,MAC 是带密钥的完整性校验,KDF 是从主密钥或口令派生密钥,数字签名用于不可否认性,证书则把公钥与身份绑定并由 CA 签名。

密码学概念 → 密码库 API 调用语义映射 左侧为概念层,右侧为典型 API 调用要素;箭头表示工程映射关系 概念层 API 调用要素 对称加密 AES / ChaCha20 key + iv/nonce plaintext → ciphertext + tag(AEAD) 非对称加密 RSA / ECIES publicKey → encrypt privateKey → decrypt padding 必须显式指定 哈希 SHA-256 / SHA-3 data → digest 无密钥,不可逆 输出长度固定 MAC HMAC / CMAC key + data → tag verify(key,data,tag) 必须常量时间比较 KDF HKDF / PBKDF2 / Argon2 ikm + salt + info → okm(指定长度) 迭代次数/内存参数 数字签名 ECDSA / Ed25519 / RSA-PSS sign(privKey, msg) verify(pubKey, msg, sig) 先哈希再签名 证书 X.509 解析/验证链 subject + pubKey + CA 签名

二、对称加密:AEAD 是默认选择

现代密码库中,对称加密接口通常分为“分组密码 + 模式”和“AEAD 一体化”两类。以 OpenSSL 的 EVP 接口为例,调用 EVP_EncryptInit_ex 时需要依次指定算法(如 EVP_aes_256_gcm())、设置 IV 长度、传入密钥和 IV。GCM 模式还需要在加密完成后调用 EVP_EncryptFinal_ex 并取出 16 字节认证标签。解密时,必须先调用 EVP_DecryptInit_ex,再通过 EVP_CIPHER_CTX_ctrl 设置期望的标签,最后在 EVP_DecryptFinal_ex 中检查返回值是否为 1,只有返回 1 才说明认证通过。

工程上要特别注意三点:第一,GCM 的 IV 绝不能重复使用,推荐 12 字节随机值,同一密钥下 IV 复用会直接导致认证密钥泄露;第二,密钥长度必须与算法匹配,AES-256 要求 32 字节密钥,传入 16 字节会被拒绝或截断;第三,AEAD 的附加认证数据(AAD)要显式传入,常用于绑定协议头、版本号等元数据,不传 AAD 时也要明确这是有意为之。

三、非对称加密与数字签名

非对称加密在 API 中通常表现为“公钥加密、私钥解密”。RSA 接口需要显式指定填充方式,如 RSA_PKCS1_OAEP_PADDING,绝不能使用已被淘汰的 PKCS#1 v1.5 加密填充。ECIES 则把临时公钥、KDF、对称加密组合成一个封装接口,调用时只需传入接收方公钥和明文。

数字签名与加密的密钥方向相反:私钥签名、公钥验签。以 Ed25519 为例,libsodium 提供 crypto_sign_detached 和 crypto_sign_verify_detached,签名输出固定 64 字节,验签返回 0 表示成功、-1 表示失败。ECDSA 则需要注意签名值的编码格式(DER 或 P1363),不同库默认格式不同,跨库交互时必须统一。RSA-PSS 需要指定盐长度,通常设为哈希输出长度。

四、哈希、MAC 与 KDF 的调用语义

哈希接口最简单,但要注意“流式”与“一次性”两种形态。SHA-256 一次性接口传入完整数据返回 32 字节摘要;流式接口则通过 init/update/final 三步处理大文件。哈希没有密钥,不能用于完整性校验的对抗场景,因为攻击者可以同时修改数据和摘要。

MAC 弥补了这一点。HMAC-SHA256 的调用语义是:传入密钥和数据,输出 32 字节标签;验证时必须使用库提供的常量时间比较函数,如 CRYPTO_memcmp,不能用普通 memcmp,否则会泄露时序信息。CMAC 则基于分组密码,适合已有 AES 密钥的场景。

KDF 用于从主密钥或口令派生多个用途密钥。HKDF 的调用语义是:输入密钥材料(IKM)、盐(salt)、上下文信息(info),输出指定长度的密钥材料(OKM)。PBKDF2 和 Argon2 用于口令派生,必须设置足够的迭代次数或内存参数,例如 PBKDF2-HMAC-SHA256 迭代 600000 次以上,Argon2id 内存 64MB、迭代 3 次。派生出的密钥要按用途分离,加密密钥和 MAC 密钥不能相同。

五、证书与信任链

证书在 API 中通常以 X.509 结构出现,解析后可以得到主体、公钥、有效期、扩展项和 CA 签名。验证证书链时,需要从终端证书逐级向上验证签名,直到受信任的根证书。工程上要检查有效期、密钥用途扩展(如 digitalSignature、keyEncipherment)、基本约束(是否为 CA)以及吊销状态(CRL 或 OCSP)。很多库提供 X509_verify_cert 或 X509_STORE_CTX 接口,但默认策略可能不检查主机名,需要额外调用 X509_check_host。

不要把哈希当 MAC 用,也不要把加密当签名用。哈希无密钥,无法抵抗主动篡改;加密保证机密性但不保证不可否认性。签名必须使用私钥,验签必须使用公钥,方向颠倒会导致安全属性完全丧失。

六、参数与调用语义对照表

概念 典型 API 形态 关键参数 常见错误
对称加密 EVP_EncryptInit_ex / update / final 算法、密钥、IV、AAD、tag IV 复用、忘记取 tag
非对称加密 RSA_public_encrypt / private_decrypt 填充方式、公钥/私钥 使用 PKCS#1 v1.5 加密填充
哈希 SHA256_Init / Update / Final 算法、输入数据 把哈希当 MAC 用
MAC HMAC_Init_ex / Update / Final 密钥、数据、输出长度 用 memcmp 比较标签
KDF HKDF_extract / expand IKM、salt、info、输出长度 盐为空、info 未绑定用途
数字签名 crypto_sign_detached / verify 私钥/公钥、消息、签名 签名与验签参数顺序颠倒
证书 X509_verify_cert / X509_check_host 信任库、主机名、用途扩展 只验签名不验主机名

七、工程调用检查清单

  1. 对称加密是否使用 AEAD 模式,IV 是否每次随机且不复用,tag 是否参与解密校验。
  2. 非对称加密是否使用 OAEP 填充,密钥长度是否满足安全强度要求(RSA ≥ 2048 位,ECC ≥ 256 位)。
  3. 哈希是否仅用于完整性摘要,未用于对抗主动篡改的场景。
  4. MAC 验证是否使用常量时间比较,密钥是否与加密密钥分离。
  5. KDF 是否设置了足够的迭代/内存参数,派生密钥是否按用途绑定 info 或标签。
  6. 签名是否先哈希再签名,验签是否检查返回值而非忽略错误。
  7. 证书链验证是否包含有效期、用途扩展、主机名和吊销状态检查。

把上述映射关系落实到代码中,核心原则是:每个密码学概念在 API 中都有明确的参数契约和错误返回语义,调用者必须显式满足这些契约,而不是依赖库的默认行为。默认值往往为了兼容性而偏保守或偏宽松,安全工程中应当主动指定算法、填充、IV 长度、标签长度和验证策略,并对每一步的返回值做严格判断。