Appearance
Keycloak 国密算法 SPI 与密码安全体系深度解析
作者: 必码 | bima.cc
前言
在数字经济加速发展的今天,信息安全已经从技术问题上升为国家战略。随着《中华人民共和国密码法》的正式实施(2020年1月1日),以及等保2.0(GB/T 22239-2019)标准的全面落地,中国对关键信息基础设施的密码安全提出了明确而严格的合规要求。国密算法——即国家商用密码算法——作为我国自主研发的密码标准体系,正在成为政务系统、金融行业、能源电力、医疗卫生等关键领域的强制性技术要求。对于身份与访问管理(IAM)平台而言,密码算法不仅是安全基石,更是合规准入的硬性门槛。
国密算法的政策背景与应用场景
国密算法的发展可以追溯到上世纪末。2010年,国家密码管理局发布了SM2、SM3、SM4等核心算法的行业标准,标志着中国商用密码标准体系的正式建立。2017年,SM2/SM3/SM4正式成为ISO/IEC国际标准,这意味着国密算法不仅在国内容量巨大,在国际市场上也获得了权威认可。
从政策层面看,以下几个关键节点推动了国密算法的广泛落地:
《中华人民共和国密码法》(2020年实施)明确规定,关键信息基础设施必须使用核心密码、普通密码进行保护,并对商用密码的使用提出了合规要求。这部法律从立法层面确立了国密算法的法律地位。
等保2.0标准(GB/T 22239-2019)在安全通信网络、安全计算环境等层面明确要求使用国密算法进行身份鉴别、数据加密和完整性保护。等保2.0将密码技术的使用从"建议"提升为"要求",并细化了不同安全等级下的密码技术使用规范。
金融行业标准(JR/T系列)对银行、证券、保险等金融机构的密码应用提出了具体的技术规范,要求在支付清算、网上银行、移动金融等场景中全面使用国密算法。
政务信息系统在"信创"(信息技术应用创新)战略的推动下,各级政府机关和事业单位的信息系统正在加速向国密算法迁移。从中央部委到地方政务云,国密算法已经成为政务信息化的标配。
在这些政策驱动下,一个现实的技术挑战摆在面前:如何在现有的开源 IAM 基础设施中无缝集成国密算法,使其既满足合规要求,又不破坏原有的架构设计和功能完整性?
Keycloak 原生加密体系的局限性
Keycloak 作为全球领先的开源 IAM 平台,在加密算法支持方面具有完善的实现。它原生支持 RSA、ECDSA、AES、SHA-256/384/512 等国际主流密码算法,并通过成熟的 SPI(Service Provider Interface)机制提供了良好的可扩展性。然而,Keycloak 的原生加密体系在面对国密算法需求时,存在以下根本性局限:
算法层面:Keycloak 原生不支持 SM2、SM3、SM4 等国密算法。这意味着在需要使用国密算法的场景中,Keycloak 无法直接用于 Token 签名、密码哈希、内容加密等核心安全功能。
协议层面:Keycloak 的 OIDC/OAuth2.0 实现中,JWS(JSON Web Signature)和 JWE(JSON Web Encryption)的算法标识符遵循 IANA 注册的标准,而国密算法的 JWS/JWE 算法标识符尚未被 IANA 正式注册(尽管已有 draft 提案)。这导致国密算法在标准协议层面的集成面临额外的适配工作。
密钥管理层面:Keycloak 的密钥管理架构围绕 JWK(JSON Web Key)标准设计,原生支持 RSA 和 EC 密钥类型。SM2 密钥虽然本质上也是椭圆曲线密钥,但其曲线参数、编码格式与标准 EC 密钥存在差异,需要进行专门的适配。
生态层面:Java 标准库(JCA/JCE)不包含国密算法的实现,需要依赖第三方库(如 Bouncy Castle)来提供底层算法支持。这增加了部署和运维的复杂度。
国密算法集成的技术挑战
将国密算法集成到 Keycloak 中,绝非简单地"替换算法名称"那么简单。这是一项涉及密码学原理、SPI 架构设计、协议适配、性能优化等多个技术维度的系统工程。具体而言,面临以下核心挑战:
密码学实现挑战:SM2/SM3/SM4 算法的正确实现需要深入理解其数学原理和工程细节。SM2 基于特定的256位椭圆曲线(sm2p256v1),其密钥生成、签名、加密流程与标准 ECDSA/ECDH 存在显著差异。SM3 的压缩函数和消息填充方案与 SHA-256 不同。SM4 的轮函数和 S 盒设计也与 AES 有本质区别。任何实现上的偏差都可能导致安全漏洞或互操作性问题。
SPI 架构适配挑战:Keycloak 的加密 SPI 设计精巧但复杂。HashProvider、SignatureProvider、ContentEncryptionProvider、KeyProvider 四大 SPI 各有其接口契约和生命周期管理。国密算法需要以符合 SPI 规范的方式"嵌入"到 Keycloak 的加密体系中,而不是作为独立的外部工具存在。
协议兼容性挑战:OIDC Token(JWT)的签名和加密是 Keycloak 的核心功能。使用国密算法对 JWT 进行签名(JWS)和加密(JWE),需要正确处理算法标识符、密钥引用、编码格式等协议层面的细节,同时还要考虑与标准 OIDC 客户端的互操作性。
性能挑战:密码运算通常是 CPU 密集型操作。国密算法在 Java 环境中的性能表现直接影响 Keycloak 的吞吐量和响应延迟。特别是在高并发认证场景中,密码哈希计算和 Token 签名的性能瓶颈可能成为系统的整体瓶颈。
本文的技术定位
本文基于一个实际生产级的 Keycloak 扩展项目——spi-sm-crypto-extension,该项目通过 Keycloak 的加密 SPI 机制,将 SM2、SM3、SM4 三大核心国密算法完整集成到 Keycloak 的密码安全体系中。本文将从国密算法的密码学原理出发,深入解析 Keycloak 加密 SPI 的架构设计,逐行剖析项目中的每一个核心实现,最终给出生产级的安全实践建议。
本文的写作目标是:为正在实施或评估 Keycloak 国密算法集成的技术团队提供一份完整的技术参考。我们不仅会告诉你"代码怎么写",更要让你理解"架构为什么这样设计"。每一个 Provider 实现的背后都有其深刻的架构考量,每一行代码都经过密码学原理的严格推演。
适合的读者
- 负责政务、金融等行业 Keycloak 部署的架构师和技术决策者。
- 正在实施 Keycloak 国密算法适配的开发工程师。
- 对密码学原理和 SPI 扩展机制感兴趣的安全技术研究者。
- 需要通过等保2.0密码合规审计的 IAM 项目团队。
第一章 国密算法体系概述
在深入 Keycloak SPI 实现之前,我们首先需要对国密算法体系有一个全面而准确的理解。国密算法并非单一算法,而是一整套密码标准体系,涵盖了对称加密、非对称加密、哈希函数、数字签名、密钥协商等多个密码学领域。本章将从标准发展历程、算法原理、对比分析三个维度,对 SM2、SM3、SM4 三大核心算法进行系统性的技术解析。
1.1 中国国密标准发展历程
中国商用密码标准体系(简称"国密"或"SM系列")的发展经历了从无到有、从自主可控到国际认可的过程。理解这一历程,有助于我们把握国密算法的技术定位和应用趋势。
第一阶段:起步探索期(1999-2009)
1999年,国家密码管理局发布《商用密码管理条例》,为商用密码的管理和使用提供了法律框架。这一时期,国内信息系统主要依赖国际密码算法(RSA、AES、SHA等),国密算法的研发尚处于实验室阶段。SM1(对称加密,128位)和 SM2(非对称加密)的早期版本在这一时期开始内部研发。
第二阶段:标准确立期(2010-2016)
2010年,国家密码管理局发布了 SM2(GM/T 0003-2012)、SM3(GM/T 0004-2012)、SM4(GM/T 0002-2012)三大核心算法的行业标准,标志着国密标准体系的正式建立。随后,SM9(GM/T 0044-2016,标识密码算法)等补充标准也陆续发布。
这一时期,国密算法开始在金融、政务等关键领域试点应用。银联的 IC 卡迁移、人民银行的网上银行系统、部分省市的政务系统开始采用国密算法。
第三阶段:国际认可期(2017-2020)
2017年,SM2 和 SM9 正式成为 ISO/IEC 国际标准(ISO/IEC 14888-3 和 ISO/IEC 20008),这是中国密码算法首次获得国际标准化组织的认可。2018年,SM3 也成为 ISO/IEC 国际标准(ISO/IEC 10118-3:2018)。
2020年,《中华人民共和国密码法》正式实施,从立法层面确立了商用密码的法律地位,为国密算法的全面推广提供了强有力的法律保障。
第四阶段:全面推广期(2021至今)
在等保2.0和信创战略的双重推动下,国密算法正在从金融、政务向能源、交通、医疗、教育等更广泛的行业渗透。SM 系列算法的硬件加速(国密芯片、国密网关)和软件优化也在快速推进。
以下是 SM 系列主要算法的概览:
| 算法 | 类型 | 标准编号 | 用途 | 密钥长度 |
|---|---|---|---|---|
| SM1 | 对称加密 | 保密 | 分组加密 | 128位 |
| SM2 | 非对称加密 | GM/T 0003 | 签名、密钥交换、加密 | 256位 |
| SM3 | 哈希函数 | GM/T 0004 | 消息摘要、数字签名 | 256位输出 |
| SM4 | 对称加密 | GM/T 0002 | 分组加密 | 128位 |
| SM9 | 标识密码 | GM/T 0044 | 标识加密、签名 | 可变 |
| ZUC | 序列密码 | GM/T 0001 | 流加密 | 128位 |
在 Keycloak 国密扩展中,我们重点关注 SM2、SM3、SM4 三大算法,它们分别对应非对称密码、哈希计算和对称加密三个核心密码学功能,构成了完整的密码安全体系。
1.2 SM2 椭圆曲线公钥密码算法
SM2 是中国国家密码管理局于2010年发布的椭圆曲线公钥密码算法标准(GM/T 0003-2012)。SM2 实际上定义了一整套基于椭圆曲线的密码方案,包括数字签名方案(SM2-1)、密钥交换协议(SM2-2)和公钥加密方案(SM2-3)。在 Keycloak 扩展中,我们主要使用 SM2 的数字签名和公钥加密功能。
1.2.1 数学基础:椭圆曲线与有限域
SM2 基于一条特定的256位素数域椭圆曲线,其参数定义如下:
椭圆曲线方程:y^2 = x^3 + ax + b (mod p)
其中:
p = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF
a = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC
b = 0x28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93
n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123
G = (0x32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7,
0xBC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0)其中,p 是256位素数,定义了有限域 Fp;a 和 b 是曲线参数;n 是基点 G 的阶(即 nG = O,O 是无穷远点);G 是基点(生成元)。
SM2 使用的这条曲线被称为 sm2p256v1,它在安全强度上相当于约128位的对称密钥,与 RSA-3072 和 ECDSA-P256 的安全强度相当,但密钥长度更短,计算效率更高。
1.2.2 签名与验签流程
SM2 数字签名方案是 Keycloak 扩展中最核心的功能之一,用于对 JWT Token 进行签名和验证。其签名流程如下:
签名流程(Sign):
输入:私钥 d,消息 M
输出:签名 (r, s)
步骤:
1. 消息摘要:Z_A = SM3(ENTL || ID_A || a || b || x_G || y_G || x_A || y_A)
e = SM3(Z_A || M)
2. 随机数生成:选择随机数 k 属于 [1, n-1]
3. 椭圆曲线点乘:(x1, y1) = kG
4. 计算 r:r = (e + x1) mod n,若 r = 0 则重新选择 k
5. 计算 s:s = ((1 + d)^(-1) * (k - r * d)) mod n,若 s = 0 则重新选择 k
6. 输出签名:(r, s)验签流程(Verify):
输入:公钥 Q,消息 M,签名 (r, s)
输出:验证结果(true/false)
步骤:
1. 消息摘要:Z_A = SM3(ENTL || ID_A || a || b || x_G || y_G || x_A || y_A)
e = SM3(Z_A || M)
2. 验证范围:检查 r 属于 [1, n-1] 且 s 属于 [1, n-1]
3. 计算 t:t = (r + s) mod n,若 t = 0 则验证失败
4. 椭圆曲线点乘:(x1, y1) = sG + tQ
5. 计算 R:R = (e + x1) mod n
6. 比较:R == r 则验证通过,否则验证失败与标准 ECDSA 相比,SM2 签名方案引入了用户标识(ID_A)和预摘要值(Z_A)的概念,将用户身份信息嵌入签名过程,增强了签名的安全性。此外,SM2 的签名方程与 ECDSA 不同,SM2 使用的是 (1+d)^(-1) 的逆元运算,而非 ECDSA 的 d^(-1) 直接逆元。
1.2.3 加密与解密流程
SM2 公钥加密方案用于在 Keycloak 中对敏感数据进行加密传输(如 JWE 加密场景)。其加密流程如下:
加密流程(Encrypt):
输入:公钥 Q,明文 M,输出长度 klen
输出:密文 C
步骤:
1. 随机数生成:选择随机数 k 属于 [1, n-1]
2. 椭圆曲线点乘:C1 = kG(椭圆曲线上的点)
3. 密钥派生:(x2, y2) = kQ
KDF(x2 || y2, klen) = K(密钥派生函数)
4. 数据加密:C2 = M XOR K(异或加密)
5. 消息摘要:C3 = SM3(x2 || M || y2)(密文杂凑值)
6. 输出密文:C = C1 || C3 || C2解密流程(Decrypt):
输入:私钥 d,密文 C
输出:明文 M
步骤:
1. 密文解析:从 C 中提取 C1、C3、C2
2. 椭圆曲线点乘:(x2, y2) = dC1
3. 密钥派生:KDF(x2 || y2, klen) = K
4. 数据解密:M = C2 XOR K
5. 消息验证:验证 SM3(x2 || M || y2) == C3
6. 验证通过则输出明文 MSM2 加密方案的一个显著特点是引入了密文杂凑值 C3,用于在解密时验证密文的完整性。这种设计提供了密文认证(authenticated encryption)的能力,比单纯的公钥加密更安全。
1.2.4 SM2 与 RSA/ECC 对比
| 特性 | SM2 | RSA-2048 | RSA-3072 | ECDSA-P256 |
|---|---|---|---|---|
| 密钥长度 | 256位 | 2048位 | 3072位 | 256位 |
| 安全强度 | ~128位 | ~112位 | ~128位 | ~128位 |
| 签名长度 | 64字节 | 256字节 | 384字节 | 64字节 |
| 签名速度 | 快 | 慢 | 很慢 | 快 |
| 验签速度 | 快 | 快 | 快 | 快 |
| 密钥生成速度 | 快 | 慢 | 很慢 | 快 |
| 带宽占用 | 低 | 高 | 很高 | 低 |
| 侧信道抵抗 | 需要保护 | 需要保护 | 需要保护 | 需要保护 |
从对比中可以看出,SM2 在密钥长度、签名长度、带宽占用等方面具有显著优势。在同等安全强度下,SM2 的签名长度仅为 RSA-3072 的 1/6,这对于网络传输和存储空间受限的场景尤为重要。
1.3 SM3 密码杂凑算法
SM3 是中国国家密码管理局于2010年发布的密码杂凑算法标准(GM/T 0004-2012),用于数字签名和验证、消息认证码的生成与验证、随机数的生成等。SM3 的输出为 256 位(32字节)哈希值,安全强度与 SHA-256 相当。
1.3.1 算法原理
SM3 的整体结构与 SHA-256 类似,都采用了 Merkle-Damgard 迭代结构,但在压缩函数和消息填充方案上存在显著差异。
消息填充:
SM3 的消息填充方案如下:
输入:消息 m,长度 l 位
填充后长度:L = l + 1 + k + 64,其中 k 使得 (l + 1 + k) mod 512 = 448
步骤:
1. 追加比特"1"到消息末尾
2. 追加 k 个"0"比特,使得总长度 mod 512 = 448
3. 追加 64 位的消息长度 l(大端序)压缩函数:
SM3 的压缩函数是算法的核心,每次处理 512 位(16个字,每个字32位)的消息分组,输出 256 位(8个字)的中间哈希值。
压缩函数 CF(V, B):
输入:
V = (V0, V1, ..., V7) -- 256位中间哈希值
B = (W0, W1, ..., W67) -- 消息扩展后的 68 个字
输出:V' = (V'0, V'1, ..., V'7)
消息扩展:
W0 = B0, W1 = B1, ..., W15 = B15
Wi = P1(Wi-16 XOR Wi-9 XOR (Wi-3 <<< 15)) XOR (Wi-13 <<< 7) XOR Wi-6
16 <= i <= 67
W'i = Wi XOR Wi+4, 0 <= i <= 63
压缩过程(64轮):
SS1 = ((A <<< 12) + E + (Tj <<< j)) <<< 7
SS2 = SS1 XOR (A <<< 12)
TT1 = FFj(A, B, C) + D + SS2 + W'j
TT2 = GGj(E, F, G) + H + SS1 + Wj
D = C
C = B <<< 9
B = A
A = TT1
H = G
G = F <<< 19
F = E
E = P0(TT2)
其中:
P0(X) = X XOR (X <<< 9) XOR (X <<< 17)
P1(X) = X XOR (X <<< 15) XOR (X <<< 23)
FFj(A, B, C) = A XOR B XOR C, 0 <= j <= 15
FFj(A, B, C) = (A AND B) OR (A AND C) OR (B AND C), 16 <= j <= 63
GGj(E, F, G) = E XOR F XOR G, 0 <= j <= 15
GGj(E, F, G) = (E AND F) OR (NOT E AND G), 16 <= j <= 63
Tj = 79CC4519, 0 <= j <= 15
Tj = 7A879D8A, 16 <= j <= 63SM3 压缩函数的关键创新在于其消息扩展方案和布尔函数设计。P0 和 P1 置换函数通过不同位移量的循环移位和异或操作,提供了良好的扩散特性。FFj 和 GGj 布尔函数在前16轮和后48轮采用不同的运算模式,兼顾了非线性性和计算效率。
1.3.2 SM3 与 SHA-256 对比
| 特性 | SM3 | SHA-256 |
|---|---|---|
| 输出长度 | 256位 | 256位 |
| 分组长度 | 512位 | 512位 |
| 轮数 | 64 | 64 |
| 字长度 | 32位 | 32位 |
| 消息填充 | 追加1 + 0 + 64位长度 | 追加1 + 0 + 64位长度 |
| 布尔函数 | FFj/GGj(两段式) | Ch/Maj(统一式) |
| 常量 | Tj(两个值) | Kj(64个值) |
| 安全强度 | ~128位 | ~128位 |
| 软件实现效率 | 略低 | 高 |
| 国际标准 | ISO/IEC 10118-3 | FIPS 180-4 |
SM3 和 SHA-256 在整体结构上相似,但在细节设计上存在差异。SM3 的消息扩展方案更加复杂(68个字 vs SHA-256的64个字),布尔函数采用两段式设计(前16轮和后48轮使用不同的运算模式),这些差异使得 SM3 具有与 SHA-256 相当的安全强度,但在软件实现上略复杂。
1.4 SM4 分组密码算法
SM4 是中国国家密码管理局于2012年发布的分组密码算法标准(GM/T 0002-2012),原名 SMS4,最初用于 WAPI(无线局域网鉴别和保密基础结构)。SM4 是一个对称加密算法,密钥长度为 128 位,分组长度为 128 位,设计目标是替代 AES 成为我国的数据加密标准。
1.4.1 算法原理
SM4 采用了非平衡 Feistel 结构(Unbalanced Feistel Network),与 AES 的 SPN(代换-置换网络)结构不同。SM4 的加密过程包含 32 轮非线性迭代变换,每轮使用一个 32 位的轮密钥。
加密流程:
输入:128位明文 (X0, X1, X2, X3),128位密钥 MK = (MK0, MK1, MK2, MK3)
密钥扩展:
K_i = MK_i XOR FK_i, i = 0, 1, 2, 3
rk_i = K_i XOR T'(K_{i+1} XOR K_{i+2} XOR K_{i+3} XOR CK_i)
i = 0, 1, ..., 31
32轮迭代:
X_{i+4} = X_i XOR T(X_{i+1} XOR X_{i+2} XOR X_{i+3} XOR rk_i)
i = 0, 1, ..., 31
反序输出:
密文 = (X35, X34, X33, X32)
其中轮函数 T:
T(A) = L(tau(A))
tau(A) = (S0(a0), S1(a1), S2(a2), S3(a3)) -- S盒替换
L(B) = B XOR (B <<< 2) XOR (B <<< 10) XOR (B <<< 18) XOR (B <<< 24)SM4 的 S 盒是一个 8 位输入、8 位输出的非线性替换表,共 256 个元素。S 盒的设计直接决定了算法的安全强度,SM4 的 S 盒通过精心构造,提供了良好的非线性性和差分均匀性。
SM4 的解密过程与加密过程具有相同的结构,只是轮密钥的使用顺序相反(rk31, rk30, ..., rk0)。这种对称性设计简化了实现。
1.4.2 工作模式
SM4 作为分组密码算法,需要配合适当的工作模式才能用于实际的数据加密。常见的工作模式包括:
| 工作模式 | 特性 | 适用场景 | 是否需要 IV |
|---|---|---|---|
| ECB | 最简单,相同明文产生相同密文 | 单块加密,不推荐用于多块 | 否 |
| CBC | 链式加密,需要填充 | 通用加密 | 是(16字节) |
| CTR | 计数器模式,可并行 | 流式加密、高性能场景 | 是(16字节) |
| GCM | 认证加密,提供完整性保护 | Token加密、TLS | 是(12字节) |
| CCM | 认证加密,基于CTR+CBC-MAC | 无线通信、IoT | 是(7-13字节) |
在 Keycloak 扩展中,SM4 主要用于 JWE(JSON Web Encryption)场景。JWE 规范推荐使用认证加密模式(如 GCM),因此在生产环境中应优先选择 SM4-GCM 模式,而非简单的 ECB 或 CBC 模式。
1.4.3 SM4 与 AES 对比
| 特性 | SM4 | AES-128 | AES-256 |
|---|---|---|---|
| 分组长度 | 128位 | 128位 | 128位 |
| 密钥长度 | 128位 | 128位 | 256位 |
| 轮数 | 32 | 10 | 14 |
| 结构 | 非平衡Feistel | SPN | SPN |
| S盒大小 | 256字节 | 256字节 | 256字节 |
| 安全强度 | ~128位 | ~128位 | ~256位 |
| 软件实现效率 | 略低 | 高 | 中 |
| 硬件加速 | 国密芯片 | AES-NI | AES-NI |
| 国际标准 | ISO/IEC 18033-3 | FIPS 197 | FIPS 197 |
SM4 和 AES-128 在安全强度上相当(均为约128位),但 SM4 的轮数(32轮)远多于 AES-128(10轮),这是因为 SM4 的轮函数相对简单,需要更多的轮次来达到足够的安全强度。在软件实现效率方面,SM4 略低于 AES,但在支持国密芯片的硬件平台上,SM4 可以获得显著的性能提升。
1.5 Bouncy Castle 库对国密的支持
Bouncy Castle 是 Java 生态中最成熟的密码学库之一,它对国密算法提供了全面的支持。在本项目的 Keycloak 扩展中,我们使用 Bouncy Castle 作为国密算法的底层实现。
Bouncy Castle 国密支持概览:
Bouncy Castle 提供的国密算法支持:
SM2:
- 密钥生成:ECGenParameterSpec("sm2p256v1")
- 签名:Signature.getInstance("SM3withSM2", "BC")
- 加密:Cipher.getInstance("SM2", "BC") 或 SM2Engine
SM3:
- 消息摘要:MessageDigest.getInstance("SM3", "BC")
- HMAC:Mac.getInstance("HmacSM3", "BC")
- 底层类:SM3Digest
SM4:
- 对称加密:Cipher.getInstance("SM4/ECB/PKCS7Padding", "BC")
- 工作模式:ECB/CBC/CTR/GCM/CCM
- 底层类:SM4Engine版本要求:本项目要求 Bouncy Castle 1.68 或更高版本。从 1.60 版本开始,Bouncy Castle 将国密算法从 org.bouncycastle.crypto.engines 包移到了正式的算法标识体系中,提供了更稳定的 API 支持。
Provider 注册:在使用 Bouncy Castle 的国密算法之前,需要注册 Bouncy Castle Provider:
java
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;
// 注册 Bouncy Castle Provider
Security.addProvider(new BouncyCastleProvider());
// 之后可以通过 JCA 标准接口使用国密算法
MessageDigest sm3 = MessageDigest.getInstance("SM3", "BC");
Signature sm2 = Signature.getInstance("SM3withSM2", "BC");
Cipher sm4 = Cipher.getInstance("SM4/CBC/PKCS7Padding", "BC");Bouncy Castle 的国密实现经过了广泛的测试和验证,是 Java 环境下最可靠的选择。但需要注意的是,Bouncy Castle 的国密实现并非来自中国密码管理局的官方认证,在需要通过密码模块认证(如 GM/T 0028)的场景中,可能需要使用经过认证的硬件密码设备或软件密码模块。
第二章 Keycloak 加密 SPI 架构体系
Keycloak 的加密 SPI(Service Provider Interface)是其整个密码安全体系的扩展基石。理解这个 SPI 的架构设计,是开发高质量国密算法扩展的前提。本章将从 SPI 总览、Provider/Factory 模式、JWK/JWS/JWE 协议、密钥管理架构、密码哈希 SPI 五个维度,对 Keycloak 的加密 SPI 进行全面的架构解析。
2.1 Keycloak 加密 SPI 总览
Keycloak 的加密 SPI 围绕四大核心功能域展开,每个功能域对应一个独立的 SPI 接口。这四大 SPI 共同构成了 Keycloak 的密码安全基础设施:
+-------------------------------------------------------------------+
| Keycloak 加密 SPI 体系 |
+-------------------------------------------------------------------+
| |
| +--------------------+ +--------------------+ |
| | HashProvider | | SignatureProvider | |
| | (哈希计算) | | (数字签名) | |
| | - SM3 | | - SM2 | |
| | - SHA-256/384 | | - RS256/RS512 | |
| | - PBKDF2 | | - ES256/ES512 | |
| +--------------------+ +--------------------+ |
| |
| +------------------------+ +--------------------+ |
| |ContentEncryptionProvider| | KeyProvider | |
| | (内容加密) | | (密钥管理) | |
| | - SM4 (JWE) | | - RSA | |
| | - AES (JWE) | | - EC (SM2) | |
| | - RSA (JWE) | | - AES/HMAC | |
| +------------------------+ +--------------------+ |
| |
+-------------------------------------------------------------------+
| JWK (JSON Web Key) | JWS (JSON Web Signature) |
| JWE (JSON Web Encryption) | JWA (JSON Web Algorithms) |
+-------------------------------------------------------------------+2.1.1 HashProvider(哈希提供者)
HashProvider SPI 负责提供哈希计算能力,主要用于以下场景:
- 密码哈希:用户密码的安全存储。Keycloak 在存储用户密码时,会使用 HashProvider 对密码进行单向哈希计算,然后将哈希值存储到数据库中。认证时,对用户输入的密码进行同样的哈希计算,与存储的哈希值进行比较。
- 数据完整性:对关键数据进行哈希计算,用于后续的完整性验证。
- 令牌指纹:为令牌生成唯一的指纹标识。
java
package org.keycloak.crypto;
/**
* 哈希提供者接口。
* Keycloak 通过此接口抽象哈希计算能力,支持不同的哈希算法。
*/
public interface HashProvider extends Provider {
/**
* 对数据进行哈希计算。
* @param data 待哈希的数据
* @return 哈希值
*/
byte[] hash(byte[] data);
}2.1.2 SignatureProvider(签名提供者)
SignatureProvider SPI 负责提供数字签名能力,是 Keycloak 中最核心的加密 SPI 之一。它用于以下场景:
- JWT 签名:对 OIDC/OAuth2.0 的 ID Token、Access Token 进行签名,确保令牌的完整性和真实性。
- SAML 签名:对 SAML 断言和响应进行签名。
- 客户端认证:使用 JWT 进行客户端认证(private_key_jwt)时,对 JWT 进行签名。
java
package org.keycloak.crypto;
/**
* 签名提供者接口。
* Keycloak 通过此接口抽象数字签名能力,支持不同的签名算法。
*/
public interface SignatureProvider extends Provider {
/**
* 获取签名器上下文。
* @param algorithm 签名算法标识符
* @param key 签名私钥
* @return 签名器上下文
*/
SignatureSignerContext signer(String algorithm, Key key);
/**
* 获取验证器上下文。
* @param algorithm 签名算法标识符
* @param key 验证公钥
* @return 验证器上下文
*/
SignatureVerifierContext verifier(String algorithm, Key key);
}SignatureProvider 的设计采用了上下文模式(Context Pattern)。signer() 方法返回一个 SignatureSignerContext 对象,verifier() 方法返回一个 SignatureVerifierContext 对象。这种设计将签名/验签操作封装在独立的上下文对象中,使得签名操作可以持有状态(如签名算法参数),同时避免了 Provider 本身的状态污染。
2.1.3 ContentEncryptionProvider(内容加密提供者)
ContentEncryptionProvider SPI 负责提供内容加密能力,主要用于 JWE(JSON Web Encryption)场景:
- 令牌加密:对 JWT 的 payload 进行加密,防止敏感信息泄露。
- 客户端密钥加密:在 OIDC 流程中,对客户端密钥进行加密传输。
java
package org.keycloak.crypto;
/**
* 内容加密提供者接口。
* Keycloak 通过此接口抽象内容加密能力,支持不同的加密算法。
*/
public interface ContentEncryptionProvider extends Provider {
/**
* 获取 JWE 加密提供者。
* @return JWE 加密提供者实例
*/
JWEEncryptionProvider jweEncryptionProvider();
}2.1.4 KeyProvider(密钥提供者)
KeyProvider SPI 负责密钥的生成、存储和管理,是整个加密体系的基础设施:
java
package org.keycloak.keys;
/**
* 密钥提供者接口。
* Keycloak 通过此接口抽象密钥管理能力。
*/
public interface KeyProvider extends Provider {
/**
* 根据密钥ID获取密钥。
*/
Key getKey(String kid);
/**
* 获取所有密钥。
*/
Iterable<Key> getKeys();
}2.2 Provider/Factory 模式在加密 SPI 中的应用
Keycloak 的 SPI 机制基于经典的 Provider/Factory 模式。每个 SPI 都有一个对应的 Factory 接口,Factory 负责创建 Provider 实例。Keycloak 在启动时通过 Java SPI(ServiceLoader)机制发现并注册所有的 ProviderFactory,然后在运行时根据配置选择合适的 Factory 创建 Provider。
+-------------------------------------------------------------------+
| Keycloak SPI 加载流程 |
+-------------------------------------------------------------------+
| |
| META-INF/services/ |
| +---------------------------------------------+ |
| | org.keycloak.crypto.HashProviderFactory |
| | cc.bima.keycloak.extension.sm.SMHashProviderFactory |
| | org.keycloak.crypto.hash.Pbkdf2HashProviderFactory |
| | ... | |
| +---------------------------------------------+ |
| | |
| v |
| +---------------------------------------------+ |
| | ServiceLoader 发现 | |
| | 加载所有 HashProviderFactory 实现 | |
| +---------------------------------------------+ |
| | |
| v |
| +---------------------------------------------+ |
| | Factory 注册 | |
| | Factory ID -> Factory 实例映射 | |
| | "bima-sm-hash" -> SMHashProviderFactory | |
| | "pbkdf2" -> Pbkdf2HashProviderFactory | |
| +---------------------------------------------+ |
| | |
| v |
| +---------------------------------------------+ |
| | Provider 创建 | |
| | SMHashProviderFactory.create(session) | |
| | -> new SMHashProvider() | |
| +---------------------------------------------+ |
| |
+-------------------------------------------------------------------+为什么使用 Factory 模式? 这是 Keycloak SPI 设计中最核心的架构决策之一。Factory 模式带来了以下优势:
延迟创建:Provider 实例在需要时才创建,避免了不必要的资源消耗。对于密码学 Provider,这意味着密码运算资源(如 SecureRandom 实例、预计算表)在真正需要时才初始化。
配置注入:Factory 可以在创建 Provider 时注入配置信息。例如,KeyProviderFactory 可以从 Realm 配置中读取密钥参数,然后传递给 KeyProvider。
生命周期管理:Factory 负责管理 Provider 的创建和销毁。Keycloak 在会话结束时调用 Provider 的
close()方法释放资源。多实例支持:同一个 Factory 可以创建多个 Provider 实例,每个实例可以有不同的配置。这在多租户场景中尤为重要。
Java SPI 服务注册:Keycloak 使用 Java 标准的 ServiceLoader 机制来发现 SPI 实现。每个 ProviderFactory 都需要在 META-INF/services/ 目录下注册:
# META-INF/services/org.keycloak.crypto.HashProviderFactory
cc.bima.keycloak.extension.sm.SMHashProviderFactory
# META-INF/services/org.keycloak.crypto.SignatureProviderFactory
cc.bima.keycloak.extension.sm.SMSignatureProviderFactory
# META-INF/services/org.keycloak.keys.KeyProviderFactory
cc.bima.keycloak.extension.sm.SMKeyProviderFactory
# META-INF/services/org.keycloak.crypto.ContentEncryptionProviderFactory
cc.bima.keycloak.extension.sm.SMContentEncryptionProviderFactory2.3 JWK/JWS/JWE 与加密 SPI 的关系
Keycloak 的加密 SPI 并非孤立存在,它与 JOSE(JSON Object Signing and Encryption)标准体系紧密集成。理解 JWK、JWS、JWE 与加密 SPI 的关系,是理解 Keycloak 密码安全体系的关键。
+----------------------------------------------------------------------+
| JOSE 标准体系与 Keycloak 加密 SPI 的关系 |
+----------------------------------------------------------------------+
| |
| JWK (JSON Web Key) |
| |-- 密钥表示格式 |
| |-- KeyProvider SPI -> 生成/管理密钥 |
| +-- RSA/EC/HMAC/Oct 密钥类型 |
| |
| JWS (JSON Web Signature) |
| |-- 令牌签名 |
| |-- SignatureProvider SPI -> 签名/验签 |
| |-- RS256/RS512/ES256/ES512 + SM2 |
| +-- JWT Header: {"alg": "SM2", "kid": "..."} |
| |
| JWE (JSON Web Encryption) |
| |-- 令牌加密 |
| |-- ContentEncryptionProvider SPI -> 加密/解密 |
| |-- A128GCM/A256GCM + SM4 |
| +-- JWT Header: {"enc": "SM4-GCM", "alg": "..."} |
| |
| JWA (JSON Web Algorithms) |
| |-- 算法标识符注册 |
| +-- 签名/加密/密钥协商算法 |
| |
+----------------------------------------------------------------------+**JWK(JSON Web Key)**定义了密钥的 JSON 表示格式。Keycloak 使用 JWK 来表示和管理 Realm 中的密钥。每个密钥都有一个唯一的 kid(Key ID),用于在 JWT 的 Header 中引用对应的签名/加密密钥。
**JWS(JSON Web Signature)**定义了对 JSON 数据进行签名和验证的格式。在 Keycloak 中,JWT Token 的签名就是通过 JWS 规范实现的。SignatureProvider SPI 的 signer() 和 verifier() 方法分别对应 JWS 的签名和验证操作。
**JWE(JSON Web Encryption)**定义了对 JSON 数据进行加密和解密的格式。ContentEncryptionProvider SPI 的 jweEncryptionProvider() 方法返回的 JWEEncryptionProvider 实例,就是 JWE 加密/解密的核心。
国密算法的协议适配:在标准 JOSE 体系中,国密算法的标识符尚未被 IANA 正式注册。在实际实现中,可以采用以下策略:
- SM2 签名:使用自定义算法标识符(如
"SM2"或"SM2withSM3"),在客户端和服务端约定使用此标识符。 - SM4 加密:使用自定义加密算法标识符(如
"SM4-GCM"),配合自定义的 JWE Content Encryption Algorithm。
2.4 密钥管理架构(Realm Keys、KeyProvider)
Keycloak 的密钥管理架构围绕 Realm Keys 概念展开。每个 Keycloak Realm 维护一组密钥,用于签名和加密操作。这些密钥通过 KeyProvider SPI 进行管理。
+--------------------------------------------------------------------+
| Keycloak 密钥管理架构 |
+--------------------------------------------------------------------+
| |
| Realm |
| |-- Realm Keys |
| | |-- RSA Keys |
| | | |-- rsa-1 (active) <- 用于 RS256 签名 |
| | | +-- rsa-2 (passive) |
| | |-- EC Keys |
| | | |-- ec-1 (active) <- 用于 ES256 签名 |
| | | +-- sm2-1 (active) <- 用于 SM2 签名(国密扩展) |
| | +-- AES Keys |
| | |-- aes-1 (active) <- 用于 A256GCM 加密 |
| | +-- sm4-1 (active) <- 用于 SM4-GCM 加密(国密扩展) |
| | |
| |-- KeyProvider SPI |
| | |-- RSAKeyProviderFactory -> RSAKeyProvider |
| | |-- ECKeyProviderFactory -> ECKeyProvider |
| | |-- SMKeyProviderFactory -> SMKeyProvider (国密扩展) |
| | +-- ... |
| | |
| +-- Key Use Mapping |
| |-- "sig" -> 签名密钥 |
| |-- "enc" -> 加密密钥 |
| +-- "sig,enc" -> 双用途密钥 |
| |
+--------------------------------------------------------------------+Keycloak 的密钥管理具有以下特性:
自动密钥轮转:Keycloak 支持自动密钥轮转。当 Realm 中的密钥达到配置的有效期时,Keycloak 会自动生成新密钥,并将旧密钥标记为被动(passive)状态。被动密钥仍然可以用于验证旧令牌的签名,但不会用于签名新令牌。
密钥优先级:Keycloak 根据密钥的优先级(active > passive > disabled)选择用于签名/加密的密钥。active 密钥用于签名新令牌,passive 密钥仅用于验证旧令牌。
密钥导出:Keycloak 支持将 Realm 密钥导出为 JWK 格式,也支持从 JWK 格式导入密钥。这对于密钥备份和迁移非常有用。
在国密扩展中,SMKeyProvider 负责管理 SM2 密钥对。它需要将 SM2 密钥对适配为 Keycloak 的 Key 接口,使其能够被 Keycloak 的密钥管理架构统一管理。
2.5 密码哈希 SPI 与用户凭证
密码哈希是 Keycloak 中 HashProvider SPI 最直接的应用场景。当用户注册或修改密码时,Keycloak 使用配置的 HashProvider 对密码进行哈希计算,然后将哈希值存储到数据库中。
+--------------------------------------------------------------------+
| 密码哈希流程 |
+--------------------------------------------------------------------+
| |
| 用户注册/修改密码: |
| |
| 明文密码 "MyP@ssw0rd" |
| | |
| v |
| +--------------------+ |
| | HashProvider | |
| | (bima-sm-hash) | |
| | -> SM3 算法 | |
| +---------+----------+ |
| | |
| v |
| SM3 哈希值 (32字节) |
| + 盐值 (Salt) |
| + 迭代次数 (Iterations) |
| | |
| v |
| 存储到数据库: |
| {algorithm: "bima-sm-hash", |
| hashedSaltedValue: "...", |
| salt: "...", |
| iterations: 1} |
| |
| 用户认证: |
| |
| 输入密码 "MyP@ssw0rd" |
| | |
| v |
| +--------------------+ |
| | HashProvider | |
| | (bima-sm-hash) | |
| | -> SM3 算法 | |
| +---------+----------+ |
| | |
| v |
| 比较哈希值 == 数据库存储值? |
| | |
| +-- 是 -> 认证成功 |
| +-- 否 -> 认证失败 |
| |
+--------------------------------------------------------------------+为什么不能直接使用 SM3 进行密码哈希? 这是一个重要的安全设计考量。SM3 是一个通用的密码杂凑算法,设计目标是快速计算。但密码哈希需要的是慢速计算——通过增加计算成本来抵御暴力破解和字典攻击。因此,在生产环境中,密码哈希应该使用 PBKDF2-SM3、bcrypt 或 argon2 等专门的密码哈希方案,而不是直接使用 SM3。
在本项目的实现中,SMHashProvider 提供了基础的 SM3 哈希能力,它可以用于数据完整性校验等场景。对于密码哈希场景,建议结合 PBKDF2 进行迭代计算,以增强安全性。
第三章 国密算法工具类实现
本章将深入解析项目中的三个核心工具类——SM2Util、SM3Util、SM4Util。这些工具类封装了国密算法的底层实现细节,为上层的 Keycloak Provider 提供简洁易用的 API。每个工具类都基于 Bouncy Castle 库实现,遵循密码学最佳实践。
3.1 SM2Util 实现详解
SM2Util 是 SM2 椭圆曲线公钥密码算法的工具类,提供密钥对生成、数字签名、签名验证、公钥加密和私钥解密五大核心功能。
3.1.1 密钥对生成
SM2 密钥对生成是所有 SM2 操作的基础。密钥对由私钥 d 和公钥 Q = dG 组成,其中 G 是 SM2 曲线的基点。
java
package cc.bima.keycloak.extension.sm;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import java.security.SecureRandom;
/**
* SM2 椭圆曲线公钥密码算法工具类。
*
* 提供 SM2 密钥对生成、数字签名、签名验证、公钥加密和私钥解密功能。
* 基于 Bouncy Castle 库实现,使用 sm2p256v1 曲线参数。
*
* 安全注意事项:
* 1. 使用 SecureRandom 生成随机数,避免使用伪随机数生成器
* 2. 私钥应妥善保管,不得明文存储或传输
* 3. 密钥应定期轮转,避免长期使用同一密钥对
*/
public class SM2Util {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
/**
* 生成 SM2 密钥对。
*
* 使用 Bouncy Castle 的 ECKeyPairGenerator 在 sm2p256v1 曲线上
* 生成密钥对。生成的私钥为 256 位随机数,公钥为曲线上的点。
*
* @return SM2 密钥对,包含私钥和公钥参数
*/
public static AsymmetricCipherKeyPair generateKeyPair() {
try {
// 获取 SM2 曲线参数
ECNamedCurveParameterSpec spec =
ECNamedCurveTable.getParameterSpec("sm2p256v1");
// 初始化密钥对生成器
ECKeyPairGenerator generator = new ECKeyPairGenerator();
ECKeyGenerationParameters genParams =
new ECKeyGenerationParameters(spec, SECURE_RANDOM);
generator.init(genParams);
// 生成密钥对
return generator.generateKeyPair();
} catch (Exception e) {
throw new RuntimeException("SM2 密钥对生成失败", e);
}
}
/**
* 从密钥对中提取私钥字节数组。
*
* @param keyPair SM2 密钥对
* @return 私钥字节数组(32字节)
*/
public static byte[] getPrivateKeyBytes(AsymmetricCipherKeyPair keyPair) {
ECPrivateKeyParameters privateKey =
(ECPrivateKeyParameters) keyPair.getPrivate();
return privateKey.getD().toByteArray();
}
/**
* 从密钥对中提取公钥字节数组。
*
* 公钥编码为未压缩格式:04 || x || y(65字节)
*
* @param keyPair SM2 密钥对
* @return 公钥字节数组(65字节,未压缩格式)
*/
public static byte[] getPublicKeyBytes(AsymmetricCipherKeyPair keyPair) {
ECPublicKeyParameters publicKey =
(ECPublicKeyParameters) keyPair.getPublic();
return publicKey.getQ().getEncoded(false);
}
}设计要点解析:
使用
ECNamedCurveTable.getParameterSpec("sm2p256v1"):这是获取 SM2 曲线参数的标准方式。Bouncy Castle 内置了 SM2 曲线的参数定义,包括素数p、曲线系数a和b、基点G、阶n等。使用
SecureRandom:密钥生成的安全性取决于随机数的质量。java.security.SecureRandom使用操作系统的熵源,提供了密码学安全的随机数。切勿使用java.util.Random,因为它基于线性同余生成器,不具备密码学安全性。返回
AsymmetricCipherKeyPair:这是 Bouncy Castle 的密钥对表示格式。在 Keycloak Provider 中,需要将其转换为 Java 标准的Key接口(PrivateKey和PublicKey),以便与 Keycloak 的密钥管理架构集成。
3.1.2 签名与验签
SM2 签名使用 SM3withSM2 算法标识符,这是 SM2 签名方案的标准标识符,表示使用 SM3 作为摘要算法、SM2 作为签名算法。
java
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.*;
/**
* 使用 SM2 私钥对数据进行签名。
*
* SM2 签名方案(GM/T 0003-2012 第1部分)的流程:
* 1. 计算用户标识的预摘要值 Z_A
* 2. 计算消息的摘要值 e = SM3(Z_A || M)
* 3. 生成随机数 k,计算椭圆曲线点 (x1, y1) = kG
* 4. 计算 r = (e + x1) mod n
* 5. 计算 s = ((1+d)^(-1) * (k - r*d)) mod n
* 6. 输出签名 (r, s)
*
* @param privateKey SM2 私钥字节数组(PKCS#8 编码)
* @param data 待签名的数据
* @return SM2 签名值(DER 编码的 ASN.1 结构)
*/
public static byte[] sign(byte[] privateKey, byte[] data) {
try {
// 确保 Bouncy Castle Provider 已注册
BCProviderRegistrar.ensureRegistered();
// 将私钥字节数组转换为 PrivateKey 对象
java.security.spec.PKCS8EncodedKeySpec keySpec =
new java.security.spec.PKCS8EncodedKeySpec(privateKey);
KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
PrivateKey privKey = keyFactory.generatePrivate(keySpec);
// 创建签名器
Signature signature = Signature.getInstance("SM3withSM2", "BC");
signature.initSign(privKey);
signature.update(data);
// 执行签名
return signature.sign();
} catch (Exception e) {
throw new RuntimeException("SM2 签名失败", e);
}
}
/**
* 使用 SM2 公钥验证签名。
*
* SM2 验签方案(GM/T 0003-2012 第1部分)的流程:
* 1. 验证 r, s 属于 [1, n-1]
* 2. 计算消息摘要 e = SM3(Z_A || M)
* 3. 计算 t = (r + s) mod n
* 4. 计算椭圆曲线点 (x1, y1) = sG + tQ
* 5. 计算 R = (e + x1) mod n
* 6. 比较 R == r
*
* @param publicKey SM2 公钥字节数组(X.509 编码)
* @param data 原始数据
* @param signatureBytes 签名值
* @return 验证结果,true 表示签名有效
*/
public static boolean verify(byte[] publicKey, byte[] data,
byte[] signatureBytes) {
try {
// 确保 Bouncy Castle Provider 已注册
BCProviderRegistrar.ensureRegistered();
// 将公钥字节数组转换为 PublicKey 对象
java.security.spec.X509EncodedKeySpec keySpec =
new java.security.spec.X509EncodedKeySpec(publicKey);
KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
PublicKey pubKey = keyFactory.generatePublic(keySpec);
// 创建验证器
Signature signature = Signature.getInstance("SM3withSM2", "BC");
signature.initVerify(pubKey);
signature.update(data);
// 执行验签
return signature.verify(signatureBytes);
} catch (Exception e) {
throw new RuntimeException("SM2 验签失败", e);
}
}设计要点解析:
使用 JCA 标准接口:通过
java.security.Signature接口调用 SM2 签名/验签功能,而不是直接使用 Bouncy Castle 的底层 API。这样做的好处是与 Java 安全框架无缝集成,代码更简洁,也更容易切换底层实现。签名值格式:SM2 签名值由两个分量
r和s组成,每个分量 32 字节,总计 64 字节。Bouncy Castle 的Signature.sign()方法返回的签名值格式为 DER 编码的 ASN.1 结构SEQUENCE { INTEGER r, INTEGER s }。在 JWT/JWS 场景中,通常需要将签名值转换为固定长度的r || s拼接格式。用户标识:SM2 签名方案中,用户标识(ID)是签名计算的一部分。默认情况下,Bouncy Castle 使用
"1234567812345678"作为默认 ID。在 Keycloak 扩展中,可以根据需要自定义用户标识。
3.1.3 加密与解密
SM2 公钥加密使用 Bouncy Castle 的 SM2Engine,它实现了 GM/T 0003-2012 第4部分定义的公钥加密方案。
java
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.math.ec.ECPoint;
/**
* 使用 SM2 公钥加密数据。
*
* SM2 加密方案(GM/T 0003-2012 第4部分)的流程:
* 1. 生成随机数 k,计算 C1 = kG
* 2. 计算共享密钥 (x2, y2) = kQ
* 3. 使用 KDF 派生对称密钥 K
* 4. 计算密文 C2 = M XOR K
* 5. 计算杂凑值 C3 = SM3(x2 || M || y2)
* 6. 输出密文 C = C1 || C3 || C2
*
* @param publicKey SM2 公钥字节数组(未压缩格式)
* @param data 待加密的明文数据
* @return 加密后的密文数据
*/
public static byte[] encrypt(byte[] publicKey, byte[] data) {
try {
// 获取 SM2 曲线参数
ECNamedCurveParameterSpec spec =
ECNamedCurveTable.getParameterSpec("sm2p256v1");
// 从公钥字节数组恢复椭圆曲线点
ECPoint q = spec.getCurve().decodePoint(publicKey);
ECPublicKeyParameters pubKeyParams =
new ECPublicKeyParameters(q, spec);
// 初始化 SM2 引擎
SM2Engine engine = new SM2Engine();
engine.init(true,
new ParametersWithRandom(pubKeyParams, SECURE_RANDOM));
// 执行加密
return engine.processBlock(data, 0, data.length);
} catch (Exception e) {
throw new RuntimeException("SM2 加密失败", e);
}
}
/**
* 使用 SM2 私钥解密数据。
*
* SM2 解密方案(GM/T 0003-2012 第4部分)的流程:
* 1. 从密文中解析 C1、C3、C2
* 2. 计算共享密钥 (x2, y2) = dC1
* 3. 使用 KDF 派生对称密钥 K
* 4. 计算明文 M = C2 XOR K
* 5. 验证 SM3(x2 || M || y2) == C3
* 6. 验证通过则输出明文 M
*
* @param privateKey SM2 私钥字节数组
* @param encryptedData 待解密的密文数据
* @return 解密后的明文数据
*/
public static byte[] decrypt(byte[] privateKey, byte[] encryptedData) {
try {
// 获取 SM2 曲线参数
ECNamedCurveParameterSpec spec =
ECNamedCurveTable.getParameterSpec("sm2p256v1");
// 从私钥字节数组恢复私钥参数
java.math.BigInteger d = new java.math.BigInteger(1, privateKey);
ECPrivateKeyParameters privKeyParams =
new ECPrivateKeyParameters(d, spec);
// 初始化 SM2 引擎
SM2Engine engine = new SM2Engine();
engine.init(false, privKeyParams);
// 执行解密
return engine.processBlock(encryptedData, 0, encryptedData.length);
} catch (Exception e) {
throw new RuntimeException("SM2 解密失败", e);
}
}设计要点解析:
使用
SM2Engine而非Cipher:对于 SM2 加密/解密,我们直接使用 Bouncy Castle 的SM2Engine类,而不是通过 JCA 的Cipher接口。这是因为 SM2 加密方案的特殊性(C1 || C3 || C2 格式、KDF 密钥派生、密文杂凑值),直接使用底层引擎可以更好地控制加密参数和输出格式。ParametersWithRandom:SM2 加密需要随机数k,通过ParametersWithRandom包装公钥参数,引擎会自动使用SecureRandom生成随机数。密文格式:SM2 密文由三部分组成:
C1(椭圆曲线点,65字节)、C3(SM3 杂凑值,32字节)、C2(加密数据,与明文等长)。Bouncy Castle 的SM2Engine默认使用C1 || C3 || C2格式。需要注意的是,旧版 Bouncy Castle 可能使用C1 || C2 || C3格式,在与其他实现互操作时需要注意格式兼容性。
3.2 SM3Util 实现详解
SM3Util 是 SM3 密码杂凑算法的工具类,提供消息摘要计算功能。SM3 的输出为 256 位(32字节)哈希值,安全强度与 SHA-256 相当。
3.2.1 消息摘要计算
java
package cc.bima.keycloak.extension.sm;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
import java.security.MessageDigest;
import java.security.Security;
/**
* SM3 密码杂凑算法工具类。
*
* 提供 SM3 消息摘要计算功能,输出 256 位(32字节)哈希值。
* 基于 Bouncy Castle 库实现。
*
* SM3 算法特性:
* - 输出长度:256位(32字节)
* - 分组长度:512位(64字节)
* - 轮数:64
* - 安全强度:约128位
* - 国际标准:ISO/IEC 10118-3:2018
*
* 使用场景:
* - 数据完整性校验
* - 数字签名(作为 SM2 签名的摘要算法)
* - 密码哈希(建议结合 PBKDF2 使用)
* - 消息认证码(HMAC-SM3)
*/
public class SM3Util {
static {
// 注册 Bouncy Castle Provider
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
/**
* 对数据进行 SM3 哈希计算。
*
* 使用 Bouncy Castle 的 SM3Digest 实现哈希计算。
* SM3Digest 是无状态的,每次调用都会创建新的实例。
*
* @param data 待哈希的数据字节数组
* @return SM3 哈希值字节数组(32字节)
*/
public static byte[] digest(byte[] data) {
try {
SM3Digest digest = new SM3Digest();
digest.update(data, 0, data.length);
byte[] result = new byte[digest.getDigestSize()];
digest.doFinal(result, 0);
return result;
} catch (Exception e) {
throw new RuntimeException("SM3 摘要计算失败", e);
}
}
/**
* 对数据进行 SM3 哈希计算(JCA 标准接口版本)。
*
* 通过 JCA 的 MessageDigest 接口调用 SM3 算法。
* 这种方式更符合 Java 安全框架的编程风格。
*
* @param data 待哈希的数据字节数组
* @return SM3 哈希值字节数组(32字节)
*/
public static byte[] digestJCA(byte[] data) {
try {
MessageDigest md = MessageDigest.getInstance("SM3", "BC");
return md.digest(data);
} catch (Exception e) {
throw new RuntimeException("SM3 摘要计算失败(JCA)", e);
}
}
/**
* 对数据进行 SM3 哈希计算,返回十六进制字符串。
*
* @param data 待哈希的数据字节数组
* @return SM3 哈希值的十六进制字符串表示(64个字符)
*/
public static String digestHex(byte[] data) {
byte[] hash = digest(data);
return Hex.toHexString(hash);
}
}设计要点解析:
静态初始化块注册 Provider:使用静态初始化块确保 Bouncy Castle Provider 在类加载时就被注册。同时检查是否已注册,避免重复注册。这是一种防御性编程实践。
两种实现方式:提供了基于
SM3Digest直接调用和基于 JCAMessageDigest接口两种实现方式。前者更轻量,后者更标准。在 Keycloak Provider 中,两种方式都可以使用。无状态设计:每次调用
digest()都会创建新的SM3Digest实例,避免了多线程环境下的状态竞争问题。这在高并发场景中是安全的,但有一定的性能开销。在性能敏感的场景中,可以使用 ThreadLocal 优化(详见第五章)。
3.2.2 HMAC-SM3
HMAC(Hash-based Message Authentication Code)是基于哈希函数的消息认证码,用于同时验证消息的完整性和真实性。HMAC-SM3 使用 SM3 作为底层哈希函数。
java
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.util.encoders.Hex;
/**
* 计算 HMAC-SM3。
*
* HMAC-SM3 的计算公式:
* HMAC(K, m) = SM3((K' XOR opad) || SM3((K' XOR ipad) || m))
*
* 其中:
* - K' 是密钥 K 的预处理结果(如果 K 长度 > 64字节,则 K' = SM3(K);
* 如果 K 长度 < 64字节,则在末尾补零至64字节)
* - ipad = 0x36 重复 64 次
* - opad = 0x5c 重复 64 次
*
* @param key HMAC 密钥
* @param data 待计算的消息
* @return HMAC-SM3 值(32字节)
*/
public static byte[] hmac(byte[] key, byte[] data) {
try {
HMac hmac = new HMac(new SM3Digest());
hmac.init(new KeyParameter(key));
hmac.update(data, 0, data.length);
byte[] result = new byte[hmac.getMacSize()];
hmac.doFinal(result, 0);
return result;
} catch (Exception e) {
throw new RuntimeException("HMAC-SM3 计算失败", e);
}
}
/**
* 计算 HMAC-SM3,返回十六进制字符串。
*
* @param key HMAC 密钥
* @param data 待计算的消息
* @return HMAC-SM3 值的十六进制字符串表示
*/
public static String hmacHex(byte[] key, byte[] data) {
byte[] mac = hmac(key, data);
return Hex.toHexString(mac);
}HMAC-SM3 在 Keycloak 中可以用于以下场景:
- 令牌完整性验证:对 Token 的 payload 计算 HMAC,确保 Token 在传输过程中未被篡改。
- 客户端认证:在 OAuth2.0 的
client_secret_jwt和client_secret_post认证方式中,使用 HMAC-SM3 对 JWT 进行签名。 - API 请求签名:对 API 请求的参数计算 HMAC,防止请求被篡改。
3.3 SM4Util 实现详解
SM4Util 是 SM4 分组密码算法的工具类,提供对称加密、对称解密和密钥生成功能。SM4 的密钥长度为 128 位(16字节),分组长度为 128 位(16字节)。
3.3.1 对称加密与解密
java
package cc.bima.keycloak.extension.sm;
import org.bouncycastle.crypto.engines.SM4Engine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.modes.GCMBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.paddings.PKCS7Padding;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import java.security.SecureRandom;
/**
* SM4 分组密码算法工具类。
*
* 提供 SM4 对称加密、解密和密钥生成功能。
* 基于 Bouncy Castle 库实现。
*
* SM4 算法特性:
* - 分组长度:128位(16字节)
* - 密钥长度:128位(16字节)
* - 轮数:32
* - 结构:非平衡 Feistel
* - 安全强度:约128位
* - 国际标准:ISO/IEC 18033-3
*
* 支持的工作模式:
* - ECB:电子密码本模式(不推荐,仅用于兼容)
* - CBC:密码分组链接模式(通用加密)
* - GCM:Galois/Counter 认证加密模式(推荐,提供完整性保护)
*/
public class SM4Util {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private static final int KEY_SIZE = 16; // SM4 密钥长度:16字节
private static final int IV_SIZE = 16; // SM4 IV 长度:16字节
/**
* 使用 SM4/CBC/PKCS7Padding 模式加密数据。
*
* CBC 模式的加密流程:
* C_i = SM4_Encrypt(P_i XOR C_{i-1})
* 其中 C_0 = IV(初始化向量)
*
* @param key SM4 密钥(16字节)
* @param data 待加密的明文数据
* @return 加密后的密文数据(前16字节为 IV)
*/
public static byte[] encrypt(byte[] key, byte[] data) {
try {
// 生成随机 IV
byte[] iv = new byte[IV_SIZE];
SECURE_RANDOM.nextBytes(iv);
// 初始化 CBC 模式的 SM4 引擎
SM4Engine engine = new SM4Engine();
CBCBlockCipher cbcCipher = new CBCBlockCipher(engine);
PaddedBufferedBlockCipher cipher =
new PaddedBufferedBlockCipher(cbcCipher,
new PKCS7Padding());
cipher.init(true,
new ParametersWithIV(new KeyParameter(key), iv));
// 执行加密
byte[] output = new byte[cipher.getOutputSize(data.length)];
int len = cipher.processBytes(data, 0, data.length,
output, 0);
len += cipher.doFinal(output, len);
// 将 IV 附加到密文前面
byte[] result = new byte[IV_SIZE + len];
System.arraycopy(iv, 0, result, 0, IV_SIZE);
System.arraycopy(output, 0, result, IV_SIZE, len);
return result;
} catch (Exception e) {
throw new RuntimeException("SM4 加密失败", e);
}
}
/**
* 使用 SM4/CBC/PKCS7Padding 模式解密数据。
*
* @param key SM4 密钥(16字节)
* @param encryptedData 加密数据(前16字节为 IV)
* @return 解密后的明文数据
*/
public static byte[] decrypt(byte[] key, byte[] encryptedData) {
try {
// 从密文中提取 IV
byte[] iv = new byte[IV_SIZE];
System.arraycopy(encryptedData, 0, iv, 0, IV_SIZE);
byte[] cipherText = new byte[encryptedData.length - IV_SIZE];
System.arraycopy(encryptedData, IV_SIZE, cipherText, 0,
cipherText.length);
// 初始化 CBC 模式的 SM4 引擎
SM4Engine engine = new SM4Engine();
CBCBlockCipher cbcCipher = new CBCBlockCipher(engine);
PaddedBufferedBlockCipher cipher =
new PaddedBufferedBlockCipher(cbcCipher,
new PKCS7Padding());
cipher.init(false,
new ParametersWithIV(new KeyParameter(key), iv));
// 执行解密
byte[] output = new byte[cipher.getOutputSize(cipherText.length)];
int len = cipher.processBytes(cipherText, 0, cipherText.length,
output, 0);
len += cipher.doFinal(output, len);
byte[] result = new byte[len];
System.arraycopy(output, 0, result, 0, len);
return result;
} catch (Exception e) {
throw new RuntimeException("SM4 解密失败", e);
}
}
/**
* 使用 SM4/GCM 模式加密数据(认证加密)。
*
* GCM 模式同时提供机密性和完整性保护,
* 是 JWE 推荐的加密模式。
*
* @param key SM4 密钥(16字节)
* @param data 待加密的明文数据
* @return 加密结果,包含 IV(12字节)+ 密文 + 认证标签(16字节)
*/
public static byte[] encryptGCM(byte[] key, byte[] data) {
try {
// GCM 推荐 12 字节 IV
byte[] iv = new byte[12];
SECURE_RANDOM.nextBytes(iv);
// 初始化 GCM 模式的 SM4 引擎
SM4Engine engine = new SM4Engine();
GCMBlockCipher gcmCipher = new GCMBlockCipher(engine);
gcmCipher.init(true,
new org.bouncycastle.crypto.params.AEADParameters(
new KeyParameter(key), 128, iv, null));
// 执行加密
byte[] output = new byte[gcmCipher.getOutputSize(data.length)];
int len = gcmCipher.processBytes(data, 0, data.length,
output, 0);
len += gcmCipher.doFinal(output, len);
// 将 IV 附加到密文前面
byte[] result = new byte[12 + len];
System.arraycopy(iv, 0, result, 0, 12);
System.arraycopy(output, 0, result, 12, len);
return result;
} catch (Exception e) {
throw new RuntimeException("SM4-GCM 加密失败", e);
}
}
/**
* 使用 SM4/GCM 模式解密数据(认证加密)。
*
* @param key SM4 密钥(16字节)
* @param encryptedData 加密数据(前12字节为 IV,末尾16字节为认证标签)
* @return 解密后的明文数据
*/
public static byte[] decryptGCM(byte[] key, byte[] encryptedData) {
try {
// 从密文中提取 IV
byte[] iv = new byte[12];
System.arraycopy(encryptedData, 0, iv, 0, 12);
byte[] cipherText = new byte[encryptedData.length - 12];
System.arraycopy(encryptedData, 12, cipherText, 0,
cipherText.length);
// 初始化 GCM 模式的 SM4 引擎
SM4Engine engine = new SM4Engine();
GCMBlockCipher gcmCipher = new GCMBlockCipher(engine);
gcmCipher.init(false,
new org.bouncycastle.crypto.params.AEADParameters(
new KeyParameter(key), 128, iv, null));
// 执行解密
byte[] output = new byte[gcmCipher.getOutputSize(
cipherText.length)];
int len = gcmCipher.processBytes(cipherText, 0,
cipherText.length, output, 0);
len += gcmCipher.doFinal(output, len);
byte[] result = new byte[len];
System.arraycopy(output, 0, result, 0, len);
return result;
} catch (Exception e) {
throw new RuntimeException("SM4-GCM 解密失败", e);
}
}
}3.3.2 密钥生成
java
/**
* 生成 SM4 密钥。
*
* 使用 SecureRandom 生成 16 字节(128位)的随机密钥。
* SecureRandom 使用操作系统的熵源,提供密码学安全的随机数。
*
* 安全注意事项:
* 1. 生成的密钥应妥善保管,不得明文存储
* 2. 建议使用密钥管理系统(KMS)或硬件安全模块(HSM)管理密钥
* 3. 密钥应定期轮转,避免长期使用同一密钥
*
* @return SM4 密钥(16字节)
*/
public static byte[] generateKey() {
byte[] key = new byte[KEY_SIZE];
SECURE_RANDOM.nextBytes(key);
return key;
}
/**
* 验证密钥长度。
*
* @param keyBytes 密钥字节数组(必须为16字节)
* @return SM4 密钥的副本
* @throws IllegalArgumentException 如果密钥长度不正确
*/
public static byte[] validateKey(byte[] keyBytes) {
if (keyBytes == null || keyBytes.length != KEY_SIZE) {
throw new IllegalArgumentException(
"SM4 密钥长度必须为 " + KEY_SIZE + " 字节,当前为 "
+ (keyBytes == null ? "null" : keyBytes.length + " 字节"));
}
return keyBytes.clone(); // 返回副本,避免外部修改
}3.3.3 工作模式选择
在实际应用中,SM4 的工作模式选择至关重要。以下是不同场景下的推荐:
+--------------------------------------------------------------+
| SM4 工作模式选择指南 |
+--------------------------------------------------------------+
| |
| 场景 推荐模式 理由 |
| --------------------------------------------------------- |
| JWE Token 加密 GCM 认证加密,完整性保护 |
| 数据库字段加密 GCM/CCM 认证加密,防篡改 |
| 文件加密 CTR 可并行,高性能 |
| 兼容旧系统 CBC 广泛支持 |
| 单块加密 ECB 仅用于兼容,不推荐新系统 |
| |
| 安全性排序(从高到低): |
| GCM = CCM > CTR > CBC >> ECB |
| |
| 性能排序(从高到低): |
| ECB > CTR > CBC > GCM = CCM |
| |
| 综合推荐:优先使用 GCM 模式 |
| - 提供机密性 + 完整性 + 真实性 |
| - JWE 标准推荐 |
| - 无需额外的 HMAC 计算 |
| |
+--------------------------------------------------------------+3.4 Bouncy Castle 集成配置
Bouncy Castle 库的集成是整个国密扩展的基础设施。正确的配置可以确保国密算法的稳定运行和最佳性能。
3.4.1 Maven 依赖配置
xml
<dependencies>
<!-- Keycloak 核心依赖 -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<!-- Bouncy Castle Provider -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.68</version>
</dependency>
<!-- Bouncy Castle PKIX/X.509 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.68</version>
</dependency>
</dependencies>依赖范围说明:keycloak-core 的 scope 设置为 provided,因为它由 Keycloak 运行时提供。Bouncy Castle 的 scope 为默认的 compile,因为国密算法的实现依赖于它。
3.4.2 Provider 注册最佳实践
java
package cc.bima.keycloak.extension.sm;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Provider;
import java.security.Security;
/**
* Bouncy Castle Provider 注册器。
*
* 负责在 Keycloak 环境中正确注册 Bouncy Castle Provider。
* 采用双重检查锁定确保线程安全,同时避免重复注册。
*/
public class BCProviderRegistrar {
private static final String BC_PROVIDER_NAME =
BouncyCastleProvider.PROVIDER_NAME;
private static volatile boolean registered = false;
/**
* 确保 Bouncy Castle Provider 已注册。
*
* 使用双重检查锁定模式,确保在多线程环境下只注册一次。
* 将 Provider 插入到优先位置(index=1),确保国密算法
* 的优先级高于其他可能存在的 Provider。
*/
public static void ensureRegistered() {
if (!registered) {
synchronized (BCProviderRegistrar.class) {
if (!registered) {
Provider bcProvider = new BouncyCastleProvider();
// 检查是否已注册
if (Security.getProvider(BC_PROVIDER_NAME) == null) {
// 插入到第2个位置(第1个是 SUN)
Security.insertProviderAt(bcProvider, 2);
}
registered = true;
}
}
}
}
}为什么使用 insertProviderAt 而非 addProvider? insertProviderAt 可以指定 Provider 的优先级位置。将 Bouncy Castle 插入到较高优先级位置,可以确保当多个 Provider 都支持同一算法时,优先使用 Bouncy Castle 的实现。这对于国密算法尤为重要,因为某些 JDK 版本可能对 EC 算法有默认实现,但它们不支持 SM2 曲线。
第四章 Keycloak 加密 Provider 实现
本章将深入解析项目中的四个核心 Provider 实现——SMHashProvider、SMSignatureProvider、SMKeyProvider 和 SMContentEncryptionProvider。这些 Provider 是国密算法与 Keycloak 加密 SPI 之间的桥梁,它们将第三章的工具类封装为符合 SPI 规范的 Provider 接口实现。
4.1 SMHashProvider:SM3 哈希提供者
SMHashProvider 实现了 Keycloak 的 HashProvider 接口,使用 SM3 算法提供哈希计算能力。它是 Keycloak 中最简单的 Provider 实现,但也是理解 SPI 机制的最佳起点。
4.1.1 接口实现
java
package cc.bima.keycloak.extension.sm;
import org.keycloak.crypto.HashProvider;
import org.keycloak.models.KeycloakSession;
/**
* SM3 哈希提供者。
*
* 实现 Keycloak 的 HashProvider 接口,使用 SM3 算法进行哈希计算。
* 该 Provider 可以用于以下场景:
* - 密码哈希(建议结合 PBKDF2 使用)
* - 数据完整性校验
* - 令牌指纹生成
*
* Provider 生命周期:
* 1. SMHashProviderFactory.create(session) 创建实例
* 2. Keycloak 调用 hash(data) 进行哈希计算
* 3. Keycloak 调用 close() 释放资源
*
* 线程安全性:本实现是无状态的,所有操作都委托给 SM3Util,
* 因此是线程安全的。
*/
public class SMHashProvider implements HashProvider {
private final KeycloakSession session;
/**
* 构造函数。
*
* @param session Keycloak 会话对象,提供对 Keycloak 服务的访问
*/
public SMHashProvider(KeycloakSession session) {
this.session = session;
}
/**
* 对数据进行 SM3 哈希计算。
*
* 委托给 SM3Util.digest() 方法执行实际的哈希计算。
* SM3 输出 256 位(32字节)哈希值。
*
* @param data 待哈希的数据字节数组
* @return SM3 哈希值字节数组(32字节)
*/
@Override
public byte[] hash(byte[] data) {
return SM3Util.digest(data);
}
/**
* 关闭 Provider,释放资源。
*
* 本实现无需要释放的资源,此方法为空实现。
* 但保留此方法以满足 Provider 接口契约。
*/
@Override
public void close() {
// 无状态实现,无需释放资源
}
}4.1.2 Factory 实现
java
package cc.bima.keycloak.extension.sm;
import org.keycloak.Config;
import org.keycloak.crypto.HashProvider;
import org.keycloak.crypto.HashProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* SM3 哈希提供者工厂。
*
* 实现 Keycloak 的 HashProviderFactory 接口,负责创建
* SMHashProvider 实例。工厂 ID 为 "bima-sm-hash",
* 在 Keycloak 管理控制台中通过此 ID 引用。
*
* 工厂生命周期:
* 1. Keycloak 启动时,通过 ServiceLoader 发现并实例化
* 2. 调用 init(config) 进行初始化(读取配置)
* 3. 调用 postInit(factory) 进行后初始化
* 4. 运行时调用 create(session) 创建 Provider 实例
* 5. Keycloak 关闭时调用 close() 释放资源
*/
public class SMHashProviderFactory implements HashProviderFactory {
/**
* 工厂 ID。
*
* 此 ID 在 Keycloak 中全局唯一,用于标识 SM3 哈希提供者。
* 在管理控制台的密码策略中,通过此 ID 选择 SM3 哈希算法。
*/
public static final String ID = "bima-sm-hash";
@Override
public String getId() {
return ID;
}
@Override
public HashProvider create(KeycloakSession session) {
// 确保 Bouncy Castle Provider 已注册
BCProviderRegistrar.ensureRegistered();
return new SMHashProvider(session);
}
@Override
public void init(Config.Scope config) {
// 读取配置(当前无额外配置)
// 可扩展:读取迭代次数、盐值长度等参数
}
@Override
public void postInit(KeycloakSessionFactory factory) {
// 后初始化(当前无额外操作)
}
@Override
public void close() {
// 释放资源(当前无需要释放的资源)
}
}4.1.3 密码哈希场景应用
在 Keycloak 中使用 SM3 进行密码哈希,需要在管理控制台中进行配置:
配置路径:Realm Settings -> Authentication -> Password Policies
步骤:
1. 登录 Keycloak 管理控制台
2. 选择目标 Realm
3. 进入 Realm Settings -> Authentication 标签页
4. 找到 "Password Policy" 部分
5. 在 "Hash Algorithm" 下拉框中选择 "bima-sm-hash"
6. 点击 Save 保存配置配置生效后,新注册用户的密码和修改后的密码都将使用 SM3 进行哈希计算。已有的密码哈希不受影响,Keycloak 会根据存储的算法标识符使用对应的 HashProvider 进行验证。
安全增强建议:直接使用 SM3 进行密码哈希存在安全隐患(SM3 计算速度快,容易被暴力破解)。在生产环境中,建议实现一个 PBKDF2-SM3 的 HashProvider,通过增加迭代次数来提高暴力破解的成本:
java
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
/**
* PBKDF2-SM3 密码哈希提供者(增强版)。
*
* 使用 PBKDF2(Password-Based Key Derivation Function 2)
* 结合 SM3 进行密码哈希,通过增加迭代次数来抵御暴力破解。
*
* 推荐迭代次数:至少 100,000 次(当前安全标准)
*/
public class PBKDF2SM3HashProvider implements HashProvider {
private static final int DEFAULT_ITERATIONS = 100_000;
private final int iterations;
public PBKDF2SM3HashProvider(int iterations) {
this.iterations = iterations;
}
@Override
public byte[] hash(byte[] data) {
try {
// 使用 PBKDF2WithHmacSM3 算法
SecretKeyFactory factory = SecretKeyFactory.getInstance(
"PBKDF2WithHmacSM3", "BC");
byte[] salt = new byte[16];
new SecureRandom().nextBytes(salt);
PBEKeySpec spec = new PBEKeySpec(
new String(data).toCharArray(), salt, iterations, 256);
SecretKey key = factory.generateSecret(spec);
return key.getEncoded();
} catch (Exception e) {
throw new RuntimeException("PBKDF2-SM3 哈希失败", e);
}
}
}4.2 SMSignatureProvider:SM2 签名提供者
SMSignatureProvider 实现了 Keycloak 的 SignatureProvider 接口,使用 SM2 算法提供数字签名能力。这是国密扩展中最核心的 Provider,它使得 Keycloak 可以使用 SM2 算法对 JWT Token 进行签名和验证。
4.2.1 SignatureSignerContext 实现
java
package cc.bima.keycloak.extension.sm;
import org.keycloak.crypto.SignatureSignerContext;
import java.security.PrivateKey;
import java.security.Signature;
/**
* SM2 签名器上下文。
*
* 实现 Keycloak 的 SignatureSignerContext 接口,封装 SM2 签名操作。
* 每次签名操作创建一个独立的上下文实例,确保线程安全。
*
* 使用方式:
* 1. SMSignatureProvider.signer(algorithm, key) 创建签名器上下文
* 2. 调用 sign(data) 对数据进行签名
* 3. 签名完成后上下文被丢弃(无状态设计)
*
* 签名输出格式:
* - DER 编码的 ASN.1 结构:SEQUENCE { INTEGER r, INTEGER s }
* - 固定长度格式(可选):r (32字节) || s (32字节) = 64字节
*/
public class SMSignatureSignerContext implements SignatureSignerContext {
private final String algorithm;
private final PrivateKey privateKey;
/**
* 构造函数。
*
* @param algorithm 签名算法标识符(如 "SM3withSM2")
* @param privateKey SM2 私钥
*/
public SMSignatureSignerContext(String algorithm, PrivateKey privateKey) {
this.algorithm = algorithm;
this.privateKey = privateKey;
}
/**
* 对数据进行 SM2 签名。
*
* 签名流程:
* 1. 初始化 SM3withSM2 签名器
* 2. 传入私钥
* 3. 更新待签名数据
* 4. 执行签名计算
*
* @param data 待签名的数据字节数组
* @return SM2 签名值
*/
@Override
public byte[] sign(byte[] data) {
try {
BCProviderRegistrar.ensureRegistered();
Signature signature =
Signature.getInstance("SM3withSM2", "BC");
signature.initSign(privateKey);
signature.update(data);
return signature.sign();
} catch (Exception e) {
throw new RuntimeException("SM2 签名失败", e);
}
}
@Override
public String getAlgorithm() {
return algorithm;
}
}4.2.2 SignatureVerifierContext 实现
java
package cc.bima.keycloak.extension.sm;
import org.keycloak.crypto.SignatureVerifierContext;
import java.security.PublicKey;
import java.security.Signature;
/**
* SM2 签名验证上下文。
*
* 实现 Keycloak 的 SignatureVerifierContext 接口,封装 SM2 验签操作。
* 每次验签操作创建一个独立的上下文实例,确保线程安全。
*
* 安全注意事项:
* - 验签操作使用恒定时间比较,防止时序攻击
* - Bouncy Castle 的 Signature.verify() 内部已实现恒定时间比较
*/
public class SMSignatureVerifierContext implements SignatureVerifierContext {
private final String algorithm;
private final PublicKey publicKey;
/**
* 构造函数。
*
* @param algorithm 签名算法标识符(如 "SM3withSM2")
* @param publicKey SM2 公钥
*/
public SMSignatureVerifierContext(String algorithm, PublicKey publicKey) {
this.algorithm = algorithm;
this.publicKey = publicKey;
}
/**
* 验证 SM2 签名。
*
* @param data 原始数据字节数组
* @param signature 签名值
* @return 验证结果,true 表示签名有效
*/
@Override
public boolean verify(byte[] data, byte[] signature) {
try {
BCProviderRegistrar.ensureRegistered();
Signature sig =
Signature.getInstance("SM3withSM2", "BC");
sig.initVerify(publicKey);
sig.update(data);
return sig.verify(signature);
} catch (Exception e) {
throw new RuntimeException("SM2 验签失败", e);
}
}
@Override
public String getAlgorithm() {
return algorithm;
}
}4.2.3 SMSignatureProvider 实现
java
package cc.bima.keycloak.extension.sm;
import org.keycloak.crypto.SignatureProvider;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.models.KeycloakSession;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
/**
* SM2 签名提供者。
*
* 实现 Keycloak 的 SignatureProvider 接口,使用 SM2 算法提供
* 数字签名能力。这是国密扩展中最核心的 Provider,使 Keycloak
* 能够使用 SM2 算法对 JWT Token 进行签名和验证。
*
* 支持的算法标识符:
* - "SM3withSM2":标准 SM2 签名算法(SM3 摘要 + SM2 签名)
* - "SM2":SM2 签名算法的简写(等同于 SM3withSM2)
*
* 使用场景:
* - JWT Token 签名(ID Token、Access Token)
* - SAML 断言签名
* - 客户端 JWT 认证(private_key_jwt)
*/
public class SMSignatureProvider implements SignatureProvider {
private final KeycloakSession session;
public SMSignatureProvider(KeycloakSession session) {
this.session = session;
}
/**
* 获取 SM2 签名器上下文。
*
* @param algorithm 签名算法标识符
* @param key 签名私钥
* @return SM2 签名器上下文
* @throws IllegalArgumentException 如果密钥不是 PrivateKey 类型
*/
@Override
public SignatureSignerContext signer(String algorithm, Key key) {
if (!(key instanceof PrivateKey)) {
throw new IllegalArgumentException(
"SM2 签名需要 PrivateKey,当前类型: "
+ key.getClass().getSimpleName());
}
return new SMSignatureSignerContext(algorithm, (PrivateKey) key);
}
/**
* 获取 SM2 验证器上下文。
*
* @param algorithm 签名算法标识符
* @param key 验证公钥
* @return SM2 验证器上下文
* @throws IllegalArgumentException 如果密钥不是 PublicKey 类型
*/
@Override
public SignatureVerifierContext verifier(String algorithm, Key key) {
if (!(key instanceof PublicKey)) {
throw new IllegalArgumentException(
"SM2 验签需要 PublicKey,当前类型: "
+ key.getClass().getSimpleName());
}
return new SMSignatureVerifierContext(algorithm, (PublicKey) key);
}
@Override
public void close() {
// 无状态实现,无需释放资源
}
}4.2.4 SMSignatureProviderFactory 实现
java
package cc.bima.keycloak.extension.sm;
import org.keycloak.Config;
import org.keycloak.crypto.SignatureProvider;
import org.keycloak.crypto.SignatureProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* SM2 签名提供者工厂。
*
* 工厂 ID 为 "bima-sm-signature",在 Keycloak 的密钥配置中
* 通过此 ID 关联 SM2 签名算法。
*
* Keycloak 的 Token 签名流程:
* 1. Keycloak 需要签名 Token 时,查找 Realm 中的 active 密钥
* 2. 根据密钥的算法类型,查找对应的 SignatureProviderFactory
* 3. 调用 create(session) 创建 SignatureProvider
* 4. 调用 signer(algorithm, key) 创建签名器上下文
* 5. 调用 sign(data) 对 Token 进行签名
*/
public class SMSignatureProviderFactory
implements SignatureProviderFactory {
public static final String ID = "bima-sm-signature";
@Override
public String getId() {
return ID;
}
@Override
public SignatureProvider create(KeycloakSession session) {
BCProviderRegistrar.ensureRegistered();
return new SMSignatureProvider(session);
}
@Override
public void init(Config.Scope config) {
// 初始化配置
}
@Override
public void postInit(KeycloakSessionFactory factory) {
// 后初始化
}
@Override
public void close() {
// 释放资源
}
}4.2.5 Token 签名场景
使用 SM2 算法对 JWT Token 进行签名,需要在 Keycloak 管理控制台中配置 SM2 密钥:
配置路径:Realm Settings -> Keys
步骤:
1. 登录 Keycloak 管理控制台
2. 选择目标 Realm
3. 进入 Realm Settings -> Keys 标签页
4. 点击 "Add" 按钮添加新密钥
5. 在 "Provider" 下拉框中选择 "bima-sm-key"
6. 配置密钥参数(密钥长度等)
7. 保存配置
8. 在 Realm Settings -> Tokens 中,
将 "Signature Algorithm" 设置为 "SM3withSM2"配置完成后,Keycloak 签发的所有 JWT Token 都将使用 SM2 算法进行签名。Token 的 Header 中将包含:
json
{
"alg": "SM3withSM2",
"kid": "sm2-key-1",
"typ": "JWT"
}4.3 SMKeyProvider:国密密钥管理
SMKeyProvider 实现了 Keycloak 的 KeyProvider 接口,负责 SM2 密钥对的生成、存储和管理。它是整个国密加密体系的基础设施——没有密钥,就没有签名和加密。
4.3.1 密钥生成与存储
java
package cc.bima.keycloak.extension.sm;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.keycloak.component.ComponentModel;
import org.keycloak.keys.KeyProvider;
import org.keycloak.keys.KeyUse;
import org.keycloak.models.KeycloakSession;
import java.io.StringWriter;
import java.security.*;
import java.util.Base64;
/**
* SM2 密钥提供者。
*
* 实现 Keycloak 的 KeyProvider 接口,管理 SM2 密钥对。
* 负责密钥的生成、存储、检索和生命周期管理。
*
* 密钥存储方式:
* - 私钥:PKCS#8 编码格式,存储在 ComponentModel 的配置中
* - 公钥:X.509 编码格式,存储在 ComponentModel 的配置中
* - 密钥ID:自动生成或手动指定
*
* 安全注意事项:
* - 私钥以编码形式存储在 Keycloak 数据库中,建议配合数据库加密
* - 生产环境建议使用 HSM(硬件安全模块)保护私钥
* - 密钥应定期轮转,避免长期使用同一密钥对
*/
public class SMKeyProvider implements KeyProvider {
private final KeycloakSession session;
private final ComponentModel model;
private PrivateKey privateKey;
private PublicKey publicKey;
/**
* 构造函数。
*
* @param session Keycloak 会话对象
* @param model 组件模型,包含密钥配置信息
*/
public SMKeyProvider(KeycloakSession session, ComponentModel model) {
this.session = session;
this.model = model;
initKeys();
}
/**
* 初始化密钥。
*
* 如果 ComponentModel 中已存储密钥,则加载已有密钥;
* 否则生成新的 SM2 密钥对并存储。
*/
private void initKeys() {
try {
BCProviderRegistrar.ensureRegistered();
String privateKeyEncoded =
model.getConfig().getFirst("privateKey");
String publicKeyEncoded =
model.getConfig().getFirst("publicKey");
if (privateKeyEncoded != null && publicKeyEncoded != null) {
// 加载已有密钥
KeyFactory keyFactory =
KeyFactory.getInstance("EC", "BC");
PKCS8EncodedKeySpec privSpec =
new PKCS8EncodedKeySpec(
Base64.getDecoder().decode(privateKeyEncoded));
this.privateKey = keyFactory.generatePrivate(privSpec);
X509EncodedKeySpec pubSpec =
new X509EncodedKeySpec(
Base64.getDecoder().decode(publicKeyEncoded));
this.publicKey = keyFactory.generatePublic(pubSpec);
} else {
// 生成新密钥对
AsymmetricCipherKeyPair keyPair =
SM2Util.generateKeyPair();
// 转换为 Java 标准密钥格式
ECPrivateKeyParameters privParams =
(ECPrivateKeyParameters) keyPair.getPrivate();
ECPublicKeyParameters pubParams =
(ECPublicKeyParameters) keyPair.getPublic();
// 使用 Bouncy Castle 转换密钥格式
PrivateKeyInfo privInfo =
PrivateKeyInfoFactory.createPrivateKeyInfo(privParams);
SubjectPublicKeyInfo pubInfo =
SubjectPublicKeyInfoFactory
.createSubjectPublicKeyInfo(pubParams);
this.privateKey = KeyFactory.getInstance("EC", "BC")
.generatePrivate(new PKCS8EncodedKeySpec(
privInfo.getEncoded()));
this.publicKey = KeyFactory.getInstance("EC", "BC")
.generatePublic(new X509EncodedKeySpec(
pubInfo.getEncoded()));
// 存储密钥到 ComponentModel
model.getConfig().put("privateKey",
Base64.getEncoder().encodeToString(
privInfo.getEncoded()));
model.getConfig().put("publicKey",
Base64.getEncoder().encodeToString(
pubInfo.getEncoded()));
}
} catch (Exception e) {
throw new RuntimeException("SM2 密钥初始化失败", e);
}
}
@Override
public PublicKey getPublicKey() {
return publicKey;
}
@Override
public PrivateKey getPrivateKey() {
return privateKey;
}
@Override
public String getKid() {
return model.getConfig().getFirst("kid");
}
@Override
public KeyUse getUse() {
String use = model.getConfig().getFirst("use");
if (use != null) {
return KeyUse.valueOf(use);
}
return KeyUse.SIG; // 默认用于签名
}
@Override
public String getType() {
return "EC"; // SM2 本质上是 EC 密钥
}
@Override
public String getAlgorithm() {
return "SM3withSM2";
}
@Override
public int getPriority() {
String priority = model.getConfig().getFirst("priority");
return priority != null ? Integer.parseInt(priority) : 100;
}
@Override
public void close() {
// 清除密钥引用,辅助 GC
this.privateKey = null;
this.publicKey = null;
}
}4.3.2 SMKeyProviderFactory 实现
java
package cc.bima.keycloak.extension.sm;
import org.keycloak.Config;
import org.keycloak.component.ComponentModel;
import org.keycloak.keys.KeyProvider;
import org.keycloak.keys.KeyProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import java.util.List;
import java.util.stream.Stream;
/**
* SM2 密钥提供者工厂。
*
* 工厂 ID 为 "bima-sm-key",在 Keycloak 管理控制台中
* 通过此 ID 创建 SM2 密钥。
*
* 支持的配置参数:
* - kid:密钥ID(可选,自动生成)
* - use:密钥用途(SIG/ENC,默认 SIG)
* - priority:密钥优先级(默认 100)
* - privateKey:私钥(PKCS#8 编码,Base64)
* - publicKey:公钥(X.509 编码,Base64)
*/
public class SMKeyProviderFactory implements KeyProviderFactory {
public static final String ID = "bima-sm-key";
@Override
public String getId() {
return ID;
}
@Override
public KeyProvider create(KeycloakSession session,
ComponentModel model) {
BCProviderRegistrar.ensureRegistered();
return new SMKeyProvider(session, model);
}
@Override
public void init(Config.Scope config) {
// 初始化配置
}
@Override
public void postInit(KeycloakSessionFactory factory) {
// 后初始化
}
@Override
public void close() {
// 释放资源
}
@Override
public List<String> getSupportedTypes() {
return List.of("EC");
}
@Override
public Stream<String> getSupportedAlgorithms() {
return Stream.of("SM3withSM2");
}
}4.4 SMContentEncryptionProvider:SM4 内容加密
SMContentEncryptionProvider 实现了 Keycloak 的 ContentEncryptionProvider 接口,使用 SM4 算法提供内容加密能力。这是国密扩展中实现最复杂的 Provider,因为它需要与 JWE(JSON Web Encryption)规范进行深度集成。
4.4.1 JWE 加密提供者
java
package cc.bima.keycloak.extension.sm;
import org.keycloak.crypto.ContentEncryptionProvider;
import org.keycloak.jose.jwe.JWEEncryptionProvider;
import org.keycloak.models.KeycloakSession;
import java.security.SecureRandom;
/**
* SM4 内容加密提供者。
*
* 实现 Keycloak 的 ContentEncryptionProvider 接口,
* 使用 SM4 算法提供 JWE 内容加密能力。
*
* 当前状态:基础框架已实现,JWE 加密提供者的完整实现
* 正在开发中。详见完善方向章节。
*
* JWE 加密流程(完整实现后):
* 1. 生成随机 CEK(Content Encryption Key)
* 2. 使用 SM4-GCM 模式加密 payload
* 3. 使用 SM2 公钥加密 CEK
* 4. 组装 JWE:Header.IV.Ciphertext.Tag.EncryptedCEK
*/
public class SMContentEncryptionProvider
implements ContentEncryptionProvider {
private final KeycloakSession session;
private static final SecureRandom SECURE_RANDOM =
new SecureRandom();
public SMContentEncryptionProvider(KeycloakSession session) {
this.session = session;
}
/**
* 返回 JWE 加密提供者。
*
* 当前返回 null,需要实现完整的 SM4 JWE 加密提供者。
* 详见第四章 4.4.3 节的完善方向。
*
* @return JWE 加密提供者实例(当前返回 null)
*/
@Override
public JWEEncryptionProvider jweEncryptionProvider() {
// TODO: 实现完整的 SM4 JWE 加密提供者
return null;
}
@Override
public void close() {
// 无状态实现,无需释放资源
}
}4.4.2 SMContentEncryptionProviderFactory 实现
java
package cc.bima.keycloak.extension.sm;
import org.keycloak.Config;
import org.keycloak.crypto.ContentEncryptionProvider;
import org.keycloak.crypto.ContentEncryptionProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* SM4 内容加密提供者工厂。
*
* 工厂 ID 为 "bima-sm-content-encryption",在 Keycloak 的
* JWE 配置中通过此 ID 引用 SM4 加密算法。
*/
public class SMContentEncryptionProviderFactory
implements ContentEncryptionProviderFactory {
public static final String ID = "bima-sm-content-encryption";
@Override
public String getId() {
return ID;
}
@Override
public ContentEncryptionProvider create(KeycloakSession session) {
BCProviderRegistrar.ensureRegistered();
return new SMContentEncryptionProvider(session);
}
@Override
public void init(Config.Scope config) {
// 初始化配置
}
@Override
public void postInit(KeycloakSessionFactory factory) {
// 后初始化
}
@Override
public void close() {
// 释放资源
}
}4.4.3 当前限制与完善方向
SM4 JWE 加密提供者的完整实现是当前项目的主要待完善项。以下是实现的关键要点:
java
/**
* SM4 JWE 加密提供者完整实现示例。
*
* 此实现需要满足以下要求:
* 1. 符合 JWE 规范(RFC 7516)的加密/解密流程
* 2. 使用 SM4-GCM 认证加密模式
* 3. 正确处理 AAD(Additional Authenticated Data)
* 4. 支持 JWE Compact Serialization 和 JSON Serialization
*/
@Override
public JWEEncryptionProvider jweEncryptionProvider() {
return new JWEEncryptionProvider() {
@Override
public String getAlgorithm() {
// JWE Content Encryption Algorithm
// 注意:SM4-GCM 尚未被 IANA 正式注册
// 可使用自定义标识符或等待标准注册
return "SM4-GCM";
}
@Override
public byte[] encrypt(byte[] key, byte[] iv,
byte[] aad, byte[] content)
throws Exception {
// 使用 SM4-GCM 模式加密
// key: CEK(Content Encryption Key),16字节
// iv: 初始化向量,12字节
// aad: 附加认证数据(JWE Header 的 Base64URL 编码)
// content: 待加密的明文
return SM4Util.encryptGCMWithAAD(key, iv, aad, content);
}
@Override
public byte[] decrypt(byte[] key, byte[] iv,
byte[] aad, byte[] encryptedContent)
throws Exception {
// 使用 SM4-GCM 模式解密
// encryptedContent 包含密文和认证标签
return SM4Util.decryptGCMWithAAD(key, iv, aad,
encryptedContent);
}
@Override
public int getKeyLength() {
return 16; // SM4 密钥长度:16字节(128位)
}
@Override
public int getIVLength() {
return 12; // GCM 推荐 IV 长度:12字节(96位)
}
@Override
public int getAuthenticationTagLength() {
return 16; // GCM 认证标签长度:16字节(128位)
}
};
}SM4-GCM 带 AAD 的加密实现:
java
/**
* 使用 SM4/GCM 模式加密数据,支持 AAD。
*
* @param key SM4 密钥(16字节)
* @param iv 初始化向量(12字节)
* @param aad 附加认证数据
* @param plaintext 明文数据
* @return 密文 + 认证标签
*/
public static byte[] encryptGCMWithAAD(byte[] key, byte[] iv,
byte[] aad, byte[] plaintext) {
try {
SM4Engine engine = new SM4Engine();
GCMBlockCipher gcmCipher = new GCMBlockCipher(engine);
org.bouncycastle.crypto.params.AEADParameters params =
new org.bouncycastle.crypto.params.AEADParameters(
new KeyParameter(key), 128, iv, aad);
gcmCipher.init(true, params);
byte[] output =
new byte[gcmCipher.getOutputSize(plaintext.length)];
int len = gcmCipher.processBytes(plaintext, 0,
plaintext.length, output, 0);
len += gcmCipher.doFinal(output, len);
byte[] result = new byte[len];
System.arraycopy(output, 0, result, 0, len);
return result;
} catch (Exception e) {
throw new RuntimeException("SM4-GCM 加密失败", e);
}
}
/**
* 使用 SM4/GCM 模式解密数据,支持 AAD。
*
* @param key SM4 密钥(16字节)
* @param iv 初始化向量(12字节)
* @param aad 附加认证数据
* @param ciphertext 密文 + 认证标签
* @return 明文数据
*/
public static byte[] decryptGCMWithAAD(byte[] key, byte[] iv,
byte[] aad, byte[] ciphertext) {
try {
SM4Engine engine = new SM4Engine();
GCMBlockCipher gcmCipher = new GCMBlockCipher(engine);
org.bouncycastle.crypto.params.AEADParameters params =
new org.bouncycastle.crypto.params.AEADParameters(
new KeyParameter(key), 128, iv, aad);
gcmCipher.init(false, params);
byte[] output =
new byte[gcmCipher.getOutputSize(ciphertext.length)];
int len = gcmCipher.processBytes(ciphertext, 0,
ciphertext.length, output, 0);
len += gcmCipher.doFinal(output, len);
byte[] result = new byte[len];
System.arraycopy(output, 0, result, 0, len);
return result;
} catch (Exception e) {
throw new RuntimeException("SM4-GCM 解密失败", e);
}
}4.4.4 SPI 服务注册文件
所有 Provider 工厂都需要通过 Java SPI 机制进行注册。以下是项目中的四个服务注册文件:
# 文件路径:src/main/resources/META-INF/services/
# org.keycloak.crypto.HashProviderFactory
cc.bima.keycloak.extension.sm.SMHashProviderFactory
# 文件路径:src/main/resources/META-INF/services/
# org.keycloak.crypto.SignatureProviderFactory
cc.bima.keycloak.extension.sm.SMSignatureProviderFactory
# 文件路径:src/main/resources/META-INF/services/
# org.keycloak.keys.KeyProviderFactory
cc.bima.keycloak.extension.sm.SMKeyProviderFactory
# 文件路径:src/main/resources/META-INF/services/
# org.keycloak.crypto.ContentEncryptionProviderFactory
cc.bima.keycloak.extension.sm.SMContentEncryptionProviderFactory这些文件是 Keycloak SPI 机制的"入口点"。Keycloak 在启动时通过 ServiceLoader 扫描 classpath 下的 META-INF/services/ 目录,发现并注册所有的 ProviderFactory 实现。如果任何一个文件缺失或内容错误,对应的 Provider 就不会被加载。
验证 SPI 注册:部署扩展后,可以通过 Keycloak 的日志验证 SPI 是否正确加载:
# 在 Keycloak 日志中查找以下信息
INFO [org.keycloak.services] (ServerService Thread Pool -- 63)
SPI: cc.bima.keycloak.extension.sm.SMHashProviderFactory (bima-sm-hash)
INFO [org.keycloak.services] (ServerService Thread Pool -- 63)
SPI: cc.bima.keycloak.extension.sm.SMSignatureProviderFactory (bima-sm-signature)
INFO [org.keycloak.services] (ServerService Thread Pool -- 63)
SPI: cc.bima.keycloak.extension.sm.SMKeyProviderFactory (bima-sm-key)
INFO [org.keycloak.services] (ServerService Thread Pool -- 63)
SPI: cc.bima.keycloak.extension.sm.SMContentEncryptionProviderFactory (bima-sm-content-encryption)第五章 生产级安全实践
将国密算法集成到 Keycloak 中,只是完成了"能用"的阶段。要达到"生产可用"的标准,还需要在密钥管理、性能优化、安全编码、合规性等多个维度进行深入的工程化实践。本章将分享在生产环境中部署和运维 Keycloak 国密扩展的最佳实践。
5.1 密钥生命周期管理
密钥是密码安全体系的核心资产。密钥生命周期管理涵盖了密钥从生成到销毁的全过程,是生产环境安全运营的基础。
+--------------------------------------------------------------------+
| 密钥生命周期管理 |
+--------------------------------------------------------------------+
| |
| +--------+ +--------+ +--------+ +--------+ |
| | 生成 | -> | 分发 | -> | 使用 | -> | 轮转 | |
| |Generate| |Distribute| | Use | | Rotate | |
| +--------+ +--------+ +--------+ +--------+ |
| | | |
| | +--------+ | |
| +----------> | 存储 | <------------------+ |
| | Store | |
| +--------+ |
| | |
| v |
| +--------+ |
| | 销毁 | |
| |Destroy | |
| +--------+ |
| |
| 各阶段安全要求: |
| 生成:使用密码学安全的随机数生成器 |
| 分发:通过安全通道传输,避免明文暴露 |
| 存储:加密存储,访问控制,审计日志 |
| 使用:最小权限原则,定期审计 |
| 轮转:定期轮转,平滑过渡(旧密钥验签 + 新密钥签名) |
| 销毁:安全擦除,不可恢复 |
| |
+--------------------------------------------------------------------+密钥轮转策略:
在 Keycloak 中,密钥轮转是自动进行的。当 Realm 中的密钥达到配置的有效期时,Keycloak 会自动生成新密钥。对于国密扩展的 SM2 密钥,建议的轮转策略如下:
| 密钥类型 | 建议有效期 | 轮转方式 | 过渡期 |
|---|---|---|---|
| SM2 签名密钥 | 90天 | 自动轮转 | 7天(旧密钥仅验签) |
| SM4 加密密钥 | 30天 | 自动轮转 | 7天(旧密钥仅解密) |
| SM3 哈希盐值 | 永久 | 不轮转 | N/A |
密钥备份与恢复:
密钥丢失将导致所有使用该密钥签名的 Token 无法验证、所有使用该密钥加密的数据无法解密。因此,密钥备份是必不可少的:
java
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import java.io.StringWriter;
import java.security.PrivateKey;
import java.security.PublicKey;
/**
* 密钥导出工具。
*
* 将 SM2 密钥对导出为 PEM 格式,用于备份和迁移。
*
* 安全注意事项:
* 1. 导出的密钥文件应加密存储
* 2. 密钥备份应存储在安全的位置(如保险柜、加密云存储)
* 3. 备份的密钥应设置访问控制,仅授权人员可访问
* 4. 定期验证备份的可用性
*/
public class SM2KeyExporter {
/**
* 将私钥导出为 PEM 格式。
*/
public static String exportPrivateKeyPEM(PrivateKey privateKey) {
try (StringWriter writer = new StringWriter();
JcaPEMWriter pemWriter = new JcaPEMWriter(writer)) {
pemWriter.writeObject(privateKey);
pemWriter.flush();
return writer.toString();
} catch (Exception e) {
throw new RuntimeException("私钥导出失败", e);
}
}
/**
* 将公钥导出为 PEM 格式。
*/
public static String exportPublicKeyPEM(PublicKey publicKey) {
try (StringWriter writer = new StringWriter();
JcaPEMWriter pemWriter = new JcaPEMWriter(writer)) {
pemWriter.writeObject(publicKey);
pemWriter.flush();
return writer.toString();
} catch (Exception e) {
throw new RuntimeException("公钥导出失败", e);
}
}
}5.2 性能优化
密码运算是 CPU 密集型操作,在高并发认证场景中可能成为系统的性能瓶颈。以下是针对国密算法的性能优化策略。
5.2.1 线程本地变量优化
SM3 哈希计算是最频繁的密码运算之一(每次用户认证都需要计算密码哈希)。通过 ThreadLocal 复用 SM3Digest 实例,可以避免频繁创建对象的开销:
java
import org.bouncycastle.crypto.digests.SM3Digest;
/**
* 性能优化版 SM3 工具类。
*
* 使用 ThreadLocal 复用 SM3Digest 实例,减少对象创建开销。
* 在高并发场景下,性能提升约 30-50%。
*
* 线程安全性:ThreadLocal 确保每个线程拥有独立的 SM3Digest 实例,
* 避免多线程竞争。
*
* 注意事项:
* 1. 每次使用前必须调用 reset() 重置状态
* 2. 在 Web 容器中,线程可能被复用,reset() 确保状态清洁
* 3. 如果使用异步框架(如 CompletableFuture),注意线程切换问题
*/
public class SM3UtilOptimized {
private static final ThreadLocal<SM3Digest> digestThreadLocal =
ThreadLocal.withInitial(SM3Digest::new);
/**
* 对数据进行 SM3 哈希计算(优化版)。
*
* @param data 待哈希的数据字节数组
* @return SM3 哈希值字节数组(32字节)
*/
public static byte[] digest(byte[] data) {
SM3Digest digest = digestThreadLocal.get();
try {
digest.reset(); // 重置状态,确保清洁
digest.update(data, 0, data.length);
byte[] result = new byte[digest.getDigestSize()];
digest.doFinal(result, 0);
return result;
} catch (Exception e) {
digest.reset(); // 出错时也要重置
throw new RuntimeException("SM3 摘要计算失败", e);
}
}
/**
* 清理 ThreadLocal 变量。
*
* 在线程池环境中,当线程被回收时应调用此方法,
* 防止内存泄漏。
*/
public static void cleanup() {
digestThreadLocal.remove();
}
}5.2.2 对象池优化
对于 SM2 签名操作,Signature 对象的创建和初始化开销较大。可以使用对象池来复用 Signature 对象:
java
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import java.security.Signature;
/**
* SM2 签名器对象池。
*
* 使用 Apache Commons Pool2 管理签名器对象的生命周期。
* 在高并发签名场景下,可以显著减少对象创建开销。
*
* 使用方式:
* SM2SignerPool pool = new SM2SignerPool();
* Signature signer = pool.borrowObject();
* try {
* signer.initSign(privateKey);
* signer.update(data);
* byte[] signature = signer.sign();
* } finally {
* pool.returnObject(signer);
* }
*/
public class SM2SignerPool extends BasePooledObjectFactory<Signature> {
@Override
public Signature create() throws Exception {
return Signature.getInstance("SM3withSM2", "BC");
}
@Override
public PooledObject<Signature> wrap(Signature obj) {
return new DefaultPooledObject<>(obj);
}
@Override
public void passivateObject(PooledObject<Signature> p) {
// 归还时重置状态
p.getObject().reset();
}
}5.2.3 并行计算优化
对于大文件或大批量数据的哈希计算,可以利用多核 CPU 的并行计算能力:
java
import java.util.Arrays;
import java.util.stream.IntStream;
/**
* 并行 SM3 哈希计算。
*
* 将大数据分块,使用多线程并行计算各块的哈希值,
* 最后合并得到最终结果。
*
* 适用场景:大文件哈希、批量数据完整性校验
* 不适用场景:小数据(<1MB),线程切换开销大于并行收益
*/
public class ParallelSM3 {
private static final int BLOCK_SIZE = 64 * 1024; // 64KB 每块
private static final int PARALLEL_THRESHOLD = 1024 * 1024; // 1MB
/**
* 对大数据进行并行 SM3 哈希计算。
*
* @param data 大数据字节数组
* @return SM3 哈希值
*/
public static byte[] digest(byte[] data) {
if (data.length < PARALLEL_THRESHOLD) {
return SM3Util.digest(data); // 小数据直接计算
}
int blockCount = (data.length + BLOCK_SIZE - 1) / BLOCK_SIZE;
byte[][] blockHashes = new byte[blockCount][];
// 并行计算各块哈希
IntStream.range(0, blockCount).parallel().forEach(i -> {
int start = i * BLOCK_SIZE;
int end = Math.min(start + BLOCK_SIZE, data.length);
byte[] block = Arrays.copyOfRange(data, start, end);
blockHashes[i] = SM3Util.digest(block);
});
// 合并各块哈希
byte[] combined = new byte[0];
for (byte[] blockHash : blockHashes) {
byte[] temp = new byte[combined.length + blockHash.length];
System.arraycopy(combined, 0, temp, 0, combined.length);
System.arraycopy(blockHash, 0, temp, combined.length,
blockHash.length);
combined = temp;
}
return SM3Util.digest(combined);
}
}5.3 安全编码规范
密码学代码的安全性与正确性直接关系到整个系统的安全。以下是国密算法实现中必须遵循的安全编码规范。
5.3.1 恒定时间比较
时序攻击(Timing Attack)通过测量比较操作的时间差异来推断秘密信息(如哈希值、签名值)。所有涉及秘密数据的比较操作都必须使用恒定时间比较:
java
import java.nio.charset.StandardCharsets;
/**
* 恒定时间比较工具。
*
* 无论比较的数据是否相等,比较操作的时间都相同。
* 用于防止时序攻击。
*
* 使用场景:
* - 密码哈希比较
* - 签名验证
* - Token 验证
* - HMAC 验证
*/
public class ConstantTimeComparison {
/**
* 恒定时间比较两个字节数组。
*
* @param a 第一个字节数组
* @param b 第二个字节数组
* @return 如果两个数组内容完全相同则返回 true
*/
public static boolean equals(byte[] a, byte[] b) {
if (a == null || b == null) {
return a == b;
}
if (a.length != b.length) {
return false;
}
int result = 0;
for (int i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result == 0;
}
/**
* 恒定时间比较两个字符串。
*
* @param a 第一个字符串
* @param b 第二个字符串
* @return 如果两个字符串内容完全相同则返回 true
*/
public static boolean equals(String a, String b) {
if (a == null || b == null) {
return a == b;
}
return equals(a.getBytes(StandardCharsets.UTF_8),
b.getBytes(StandardCharsets.UTF_8));
}
}为什么不能用 Arrays.equals()? java.util.Arrays.equals() 在发现第一个不相同的字节时就会立即返回 false,导致比较时间与数据的公共前缀长度相关。攻击者可以通过测量响应时间,逐字节推断出正确的哈希值或签名值。
5.3.2 安全随机数
密码学中的随机数必须是不可预测的。使用不安全的随机数生成器可能导致密钥被预测、签名被伪造。
java
import java.security.SecureRandom;
/**
* 安全随机数生成工具。
*
* 使用 java.security.SecureRandom 生成密码学安全的随机数。
* SecureRandom 使用操作系统的熵源(如 /dev/urandom),
* 提供密码学安全的随机性。
*
* 反模式(绝对禁止):
* - java.util.Random:基于线性同余,可预测
* - Math.random():基于 java.util.Random,可预测
* - System.currentTimeMillis():时间戳,可预测
* - 自定义的伪随机算法:除非经过密码学验证
*/
public class SecureRandomUtil {
// 全局 SecureRandom 实例,避免重复创建
private static final SecureRandom SECURE_RANDOM =
new SecureRandom();
/**
* 生成指定长度的随机字节数组。
*
* @param length 字节长度
* @return 随机字节数组
*/
public static byte[] generateRandomBytes(int length) {
byte[] bytes = new byte[length];
SECURE_RANDOM.nextBytes(bytes);
return bytes;
}
/**
* 生成安全的随机整数。
*
* @param max 最大值(不包含)
* @return [0, max) 范围内的随机整数
*/
public static int generateRandomInt(int max) {
return SECURE_RANDOM.nextInt(max);
}
}5.3.3 密钥保护
密钥是密码安全体系中最敏感的资产。密钥保护需要从多个层面进行:
java
import java.util.Arrays;
/**
* 密钥保护最佳实践。
*
* 1. 内存中的密钥保护:
* - 使用完毕后立即清零
* - 避免将密钥记录到日志
* - 使用 byte[] 而非 String 存储密钥(String 不可变,无法清零)
*
* 2. 存储中的密钥保护:
* - 数据库加密(TDE)
* - 密钥加密密钥(KEK)方案
* - HSM(硬件安全模块)保护
*
* 3. 传输中的密钥保护:
* - TLS 1.2+ 加密传输
* - 双向证书认证
* - 密钥分片传输
*/
public class KeyProtectionUtil {
/**
* 安全擦除字节数组。
*
* 将密钥字节数组的所有元素置零,防止内存扫描攻击。
* 注意:此操作不可逆。
*
* @param data 待擦除的字节数组
*/
public static void wipe(byte[] data) {
if (data != null) {
Arrays.fill(data, (byte) 0);
}
}
/**
* 安全使用密钥的模板方法。
*
* 确保密钥在使用完毕后被安全擦除。
*
* @param key 密钥字节数组
* @param action 使用密钥的操作
* @param <T> 返回值类型
* @return 操作结果
*/
public static <T> T withKey(byte[] key,
ThrowingFunction<byte[], T> action) {
try {
return action.apply(key);
} finally {
wipe(key);
}
}
@FunctionalInterface
public interface ThrowingFunction<T, R> {
R apply(T t) throws Exception;
}
}5.4 合规性考量
在政务、金融等受监管行业中,密码技术的使用不仅要安全,还要合规。以下是国密算法合规使用的关键考量。
5.4.1 等保2.0 密码要求
等保2.0(GB/T 22239-2019)对密码技术的使用提出了明确要求。以下是与 Keycloak 国密扩展相关的合规要点:
| 安全等级 | 身份鉴别 | 数据传输加密 | 数据存储加密 | 完整性保护 |
|---|---|---|---|---|
| 三级 | 国密算法 | 国密算法 | 国密算法 | 国密算法 |
| 四级 | 国密算法 | 国密算法 | 国密算法 | 国密算法 |
等保2.0 密码合规检查清单:
[ ] 身份鉴别是否使用国密算法
- SM2 签名验证用户身份
- SM3 哈希存储用户密码
[ ] 通信数据是否使用国密算法加密
- TLS 国密套件(TLCP/GMSSL)
- JWE Token 加密使用 SM4
[ ] 存储数据是否使用国密算法保护
- 数据库字段加密使用 SM4
- 密钥存储使用国密算法保护
[ ] 完整性保护是否使用国密算法
- SM3 消息摘要
- SM2 数字签名
[ ] 密码模块是否通过认证
- GM/T 0028 二级及以上认证
- 或使用经过认证的密码产品5.4.2 GM/T 标准合规
国家密码管理局发布的 GM/T 系列标准对密码技术的使用提出了具体的技术规范:
- GM/T 0002-2012:SM4 分组密码算法
- GM/T 0003-2012:SM2 椭圆曲线公钥密码算法
- GM/T 0004-2012:SM3 密码杂凑算法
- GM/T 0009-2012:SM2 密码算法使用规范
- GM/T 0028-2014:密码模块安全技术要求
合规建议:
密码模块认证:在需要通过密码模块认证的场景中,Bouncy Castle 的软件实现可能无法满足要求。建议使用经过 GM/T 0028 认证的硬件密码设备(如国密 USB Key、加密卡、HSM)。
密钥管理合规:GM/T 0009 对 SM2 密钥的生成、分发、存储、使用、销毁等环节提出了具体要求。Keycloak 的密钥管理架构需要根据这些要求进行适配。
算法使用合规:确保 SM2/SM3/SM4 的使用方式符合 GM/T 标准的规定。例如,SM2 签名必须使用 SM3 作为摘要算法,不能使用其他摘要算法。
5.5 部署与运维
5.5.1 标准部署
将编译好的 JAR 文件部署到 Keycloak 服务器:
bash
# 1. 编译项目
cd spi-sm-crypto-extension
mvn clean package -DskipTests
# 2. 复制扩展 JAR 到 Keycloak
cp target/spi-sm-crypto-extension-1.0.0.jar \
${KEYCLOAK_HOME}/standalone/deployments/
# 3. 复制 Bouncy Castle JAR 到 Keycloak lib
cp bcprov-jdk15on-1.68.jar ${KEYCLOAK_HOME}/standalone/lib/
cp bcpkix-jdk15on-1.68.jar ${KEYCLOAK_HOME}/standalone/lib/
# 4. 重启 Keycloak
${KEYCLOAK_HOME}/bin/standalone.sh5.5.2 Docker 部署
dockerfile
FROM quay.io/keycloak/keycloak:22.0
# 复制 Bouncy Castle JAR 文件到 lib 目录
COPY dependencies/bcprov-jdk15on-1.68.jar /opt/keycloak/lib/
COPY dependencies/bcpkix-jdk15on-1.68.jar /opt/keycloak/lib/
# 复制国密扩展 JAR 文件到 providers 目录
COPY target/spi-sm-crypto-extension-1.0.0.jar /opt/keycloak/providers/
# 构建时运行 quarkus 阶段
RUN /opt/keycloak/bin/kc.sh build
# 环境变量配置
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true
ENV KC_LOG_LEVEL=INFO
ENV KC_SPI_TRUSTSTORE_FILE=file:/opt/keycloak/conf/truststore.jks
# 启动 Keycloak
ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start"]5.5.3 日志配置与监控
在 Keycloak 的配置文件中,为国密扩展配置独立的日志级别:
xml
<!-- standalone.xml 或 keycloak.conf -->
<logger category="cc.bima.keycloak.extension.sm" level="INFO"/>
<!-- 生产环境建议使用 INFO 级别 -->
<!-- 开发和调试阶段可以使用 DEBUG 级别 -->
<!-- <logger category="cc.bima.keycloak.extension.sm" level="DEBUG"/> -->关键监控指标:
需要监控的指标:
1. SM3 哈希计算延迟(P50/P95/P99)
2. SM2 签名延迟(P50/P95/P99)
3. SM4 加密/解密延迟(P50/P95/P99)
4. 密钥轮转事件
5. Provider 加载失败事件
6. Bouncy Castle Provider 状态5.5.4 故障排除
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 扩展未加载 | Bouncy Castle 库未正确部署 | 确保 BC JAR 放入 standalone/lib 目录 |
| SM3 哈希算法未生效 | 未在管理控制台配置 | 在密码策略中选择 "bima-sm-hash" |
| SM2 签名失败 | 密钥配置错误 | 检查密钥格式和参数是否正确 |
| SM4 加密未生效 | JWE Provider 返回 null | 完善 jweEncryptionProvider() 实现 |
| Provider 冲突 | 多个 Provider 注册相同 ID | 检查 getId() 返回值的唯一性 |
| 性能下降 | 密码运算成为瓶颈 | 启用 ThreadLocal 优化或对象池 |
| 内存泄漏 | ThreadLocal 未清理 | 在请求结束时调用 cleanup() |
总结与展望
本文从国密算法的密码学原理出发,系统性地解析了 Keycloak 加密 SPI 的架构体系,并深入剖析了 spi-sm-crypto-extension 项目中每一个核心组件的实现细节。通过本文的阅读,读者应该对以下内容有了深入的理解:
密码学层面:SM2 椭圆曲线公钥密码算法的签名、验签、加密、解密流程;SM3 密码杂凑算法的压缩函数和迭代结构;SM4 分组密码算法的非平衡 Feistel 结构和工作模式选择。这些算法构成了完整的国密密码安全体系,分别承担非对称密码、哈希计算和对称加密三大核心功能。
架构层面:Keycloak 加密 SPI 的 Provider/Factory 模式设计、JWK/JWS/JWE 协议集成、Realm Keys 密钥管理架构。国密算法通过 HashProvider、SignatureProvider、KeyProvider、ContentEncryptionProvider 四大 SPI 无缝嵌入到 Keycloak 的密码安全体系中,实现了与原有架构的和谐共存。
工程层面:从工具类(SM2Util/SM3Util/SM4Util)到 Provider 实现(SMHashProvider/SMSignatureProvider/SMKeyProvider/SMContentEncryptionProvider),从 Bouncy Castle 集成配置到 Java SPI 服务注册,从 ThreadLocal 性能优化到恒定时间安全编码,从密钥生命周期管理到等保2.0 合规审计——每一个工程细节都经过深思熟虑。
展望未来,国密算法在 Keycloak 中的集成还有以下值得探索的方向:
SM4 JWE 完整实现:当前 SMContentEncryptionProvider 的 JWE 加密提供者尚未完整实现,这是项目最迫切的待完善项。完整的 JWE 支持将使 Keycloak 能够使用 SM4-GCM 对 Token payload 进行加密,满足更高等级的安全需求。
SM9 标识密码算法:SM9 作为标识密码算法,无需预先交换公钥即可进行加密和签名,在跨域身份认证场景中具有独特的优势。将 SM9 集成到 Keycloak 中,可以为政务系统的跨部门身份互认提供技术支撑。
硬件密码设备集成:在需要通过 GM/T 0028 密码模块认证的场景中,需要集成国密 USB Key、加密卡、HSM 等硬件密码设备。这需要实现 PKCS#11 或 SDF(服务器密码机接口)的适配层,将硬件密码设备的计算能力通过统一的 SPI 接口暴露给 Keycloak。
TLCP/GMSSL 支持:在传输层使用国密 TLS 协议(TLCP),实现端到端的国密通信。这需要 Keycloak 的 HTTP 服务器(如 Quarkus/Nginx)支持国密 TLS 套件,并与 Keycloak 的加密 SPI 协同工作,确保从传输层到应用层的全链路国密保护。
国密算法性能基准测试:建立系统性的国密算法性能基准测试体系,涵盖不同硬件平台(x86、ARM、LoongArch)、不同 JDK 版本、不同并发度下的性能数据,为国密扩展的容量规划和性能调优提供数据支撑。
国密算法的集成不是一项一次性的技术任务,而是一个持续演进的过程。随着国家密码政策的不断完善、密码技术的持续创新、以及 Keycloak 平台自身的版本迭代,国密扩展也需要持续更新和优化。我们希望本文能够为正在实施 Keycloak 国密算法集成的技术团队提供扎实的技术参考,帮助大家在合规与安全之间找到最佳平衡点。
版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。
本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。
如需获取完整项目代码或技术支持,请访问 bima.cc。