Skip to content

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位素数,定义了有限域 Fpab 是曲线参数;n 是基点 G 的阶(即 nG = OO 是无穷远点);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. 验证通过则输出明文 M

SM2 加密方案的一个显著特点是引入了密文杂凑值 C3,用于在解密时验证密文的完整性。这种设计提供了密文认证(authenticated encryption)的能力,比单纯的公钥加密更安全。

1.2.4 SM2 与 RSA/ECC 对比

特性SM2RSA-2048RSA-3072ECDSA-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 <= 63

SM3 压缩函数的关键创新在于其消息扩展方案和布尔函数设计。P0 和 P1 置换函数通过不同位移量的循环移位和异或操作,提供了良好的扩散特性。FFj 和 GGj 布尔函数在前16轮和后48轮采用不同的运算模式,兼顾了非线性性和计算效率。

1.3.2 SM3 与 SHA-256 对比

特性SM3SHA-256
输出长度256位256位
分组长度512位512位
轮数6464
字长度32位32位
消息填充追加1 + 0 + 64位长度追加1 + 0 + 64位长度
布尔函数FFj/GGj(两段式)Ch/Maj(统一式)
常量Tj(两个值)Kj(64个值)
安全强度~128位~128位
软件实现效率略低
国际标准ISO/IEC 10118-3FIPS 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 对比

特性SM4AES-128AES-256
分组长度128位128位128位
密钥长度128位128位256位
轮数321014
结构非平衡FeistelSPNSPN
S盒大小256字节256字节256字节
安全强度~128位~128位~256位
软件实现效率略低
硬件加速国密芯片AES-NIAES-NI
国际标准ISO/IEC 18033-3FIPS 197FIPS 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 模式带来了以下优势:

  1. 延迟创建:Provider 实例在需要时才创建,避免了不必要的资源消耗。对于密码学 Provider,这意味着密码运算资源(如 SecureRandom 实例、预计算表)在真正需要时才初始化。

  2. 配置注入:Factory 可以在创建 Provider 时注入配置信息。例如,KeyProviderFactory 可以从 Realm 配置中读取密钥参数,然后传递给 KeyProvider。

  3. 生命周期管理:Factory 负责管理 Provider 的创建和销毁。Keycloak 在会话结束时调用 Provider 的 close() 方法释放资源。

  4. 多实例支持:同一个 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.SMContentEncryptionProviderFactory

2.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);
    }
}

设计要点解析

  1. 使用 ECNamedCurveTable.getParameterSpec("sm2p256v1"):这是获取 SM2 曲线参数的标准方式。Bouncy Castle 内置了 SM2 曲线的参数定义,包括素数 p、曲线系数 ab、基点 G、阶 n 等。

  2. 使用 SecureRandom:密钥生成的安全性取决于随机数的质量。java.security.SecureRandom 使用操作系统的熵源,提供了密码学安全的随机数。切勿使用 java.util.Random,因为它基于线性同余生成器,不具备密码学安全性。

  3. 返回 AsymmetricCipherKeyPair:这是 Bouncy Castle 的密钥对表示格式。在 Keycloak Provider 中,需要将其转换为 Java 标准的 Key 接口(PrivateKeyPublicKey),以便与 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);
    }
}

设计要点解析

  1. 使用 JCA 标准接口:通过 java.security.Signature 接口调用 SM2 签名/验签功能,而不是直接使用 Bouncy Castle 的底层 API。这样做的好处是与 Java 安全框架无缝集成,代码更简洁,也更容易切换底层实现。

  2. 签名值格式:SM2 签名值由两个分量 rs 组成,每个分量 32 字节,总计 64 字节。Bouncy Castle 的 Signature.sign() 方法返回的签名值格式为 DER 编码的 ASN.1 结构 SEQUENCE { INTEGER r, INTEGER s }。在 JWT/JWS 场景中,通常需要将签名值转换为固定长度的 r || s 拼接格式。

  3. 用户标识: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);
    }
}

设计要点解析

  1. 使用 SM2Engine 而非 Cipher:对于 SM2 加密/解密,我们直接使用 Bouncy Castle 的 SM2Engine 类,而不是通过 JCA 的 Cipher 接口。这是因为 SM2 加密方案的特殊性(C1 || C3 || C2 格式、KDF 密钥派生、密文杂凑值),直接使用底层引擎可以更好地控制加密参数和输出格式。

  2. ParametersWithRandom:SM2 加密需要随机数 k,通过 ParametersWithRandom 包装公钥参数,引擎会自动使用 SecureRandom 生成随机数。

  3. 密文格式: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);
    }
}

设计要点解析

  1. 静态初始化块注册 Provider:使用静态初始化块确保 Bouncy Castle Provider 在类加载时就被注册。同时检查是否已注册,避免重复注册。这是一种防御性编程实践。

  2. 两种实现方式:提供了基于 SM3Digest 直接调用和基于 JCA MessageDigest 接口两种实现方式。前者更轻量,后者更标准。在 Keycloak Provider 中,两种方式都可以使用。

  3. 无状态设计:每次调用 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_jwtclient_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:密码模块安全技术要求

合规建议

  1. 密码模块认证:在需要通过密码模块认证的场景中,Bouncy Castle 的软件实现可能无法满足要求。建议使用经过 GM/T 0028 认证的硬件密码设备(如国密 USB Key、加密卡、HSM)。

  2. 密钥管理合规:GM/T 0009 对 SM2 密钥的生成、分发、存储、使用、销毁等环节提出了具体要求。Keycloak 的密钥管理架构需要根据这些要求进行适配。

  3. 算法使用合规:确保 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.sh

5.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 中的集成还有以下值得探索的方向:

  1. SM4 JWE 完整实现:当前 SMContentEncryptionProvider 的 JWE 加密提供者尚未完整实现,这是项目最迫切的待完善项。完整的 JWE 支持将使 Keycloak 能够使用 SM4-GCM 对 Token payload 进行加密,满足更高等级的安全需求。

  2. SM9 标识密码算法:SM9 作为标识密码算法,无需预先交换公钥即可进行加密和签名,在跨域身份认证场景中具有独特的优势。将 SM9 集成到 Keycloak 中,可以为政务系统的跨部门身份互认提供技术支撑。

  3. 硬件密码设备集成:在需要通过 GM/T 0028 密码模块认证的场景中,需要集成国密 USB Key、加密卡、HSM 等硬件密码设备。这需要实现 PKCS#11 或 SDF(服务器密码机接口)的适配层,将硬件密码设备的计算能力通过统一的 SPI 接口暴露给 Keycloak。

  4. TLCP/GMSSL 支持:在传输层使用国密 TLS 协议(TLCP),实现端到端的国密通信。这需要 Keycloak 的 HTTP 服务器(如 Quarkus/Nginx)支持国密 TLS 套件,并与 Keycloak 的加密 SPI 协同工作,确保从传输层到应用层的全链路国密保护。

  5. 国密算法性能基准测试:建立系统性的国密算法性能基准测试体系,涵盖不同硬件平台(x86、ARM、LoongArch)、不同 JDK 版本、不同并发度下的性能数据,为国密扩展的容量规划和性能调优提供数据支撑。

国密算法的集成不是一项一次性的技术任务,而是一个持续演进的过程。随着国家密码政策的不断完善、密码技术的持续创新、以及 Keycloak 平台自身的版本迭代,国密扩展也需要持续更新和优化。我们希望本文能够为正在实施 Keycloak 国密算法集成的技术团队提供扎实的技术参考,帮助大家在合规与安全之间找到最佳平衡点。


版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。

本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。

如需获取完整项目代码或技术支持,请访问 bima.cc