Ohhnews

分类导航

$ cd ..
InfoQ Java原文

Spring Boot中的后量子密码学:本迭代可落地的四种模式

#后量子密码学#spring boot#数据加密#数字签名#密钥管理

核心要点

  • 如果你已经在使用 JDK 24,就可以直接通过标准 Java 加密扩展(JCE)API 使用基于模格的密钥封装机制(ML-KEM)和基于模格的数字签名算法(ML-DSA),无需额外引入库。你原本可能就在规划的 JDK 升级(JDK 24),也会一并打通整个后量子密码学(PQC)迁移之路。
  • 现在正有人保存你用 RSA 封装的 TLS 会话,打算以后解密。因此,今天在你的服务之间流动的任何客户社会安全号码(SSN)、交易记录和“了解你的客户”(KYC)文档,都已经处于风险之中。等待云服务商提供 PQC TLS,并不是一个迁移计划。
  • 用 Kyber 密钥加密数据库字段并不难;真正的难点在于确保该密钥不会驻留在 JVM 堆中。因为一次服务器重启或一次堆转储,就会让数据库中所有加密行无法读取。在部署到生产环境之前,必须先集成密钥管理服务(KMS)或 HashiCorp Vault。
  • 核心银行、欺诈检测和监管报告流水线中使用的 OAuth2 令牌和服务账户凭据,通常有效期长达数月甚至数年,因此它们比短期客户会话令牌更需要优先进行 PQC 迁移。
  • 从生命周期最长的数据开始 PQC 迁移,而不是从你最熟悉的授权层开始。因为今天用 RSA 签署的贷款协议和 KYC 记录,到 2035 年左右签名将可以被伪造,届时你无法对已归档的文档进行追溯重签。

背景

2024 年 8 月,NIST 最终确定了 FIPS 203FIPS 204 规范之后,受监管行业中大多数工程团队开始问同一个问题:“我们到底该从哪里开始?”显而易见的答案是“切换到 PQC TLS”,但这一能力仍在云服务商中逐步推出,大多数团队今天还无法启用。与此同时,真正的风险已经在发生:攻击者现在就在截获并存储你的加密服务间流量,以便将来量子硬件成熟后解密。

考虑一个典型的零售银行微服务平台,它基于 Spring Boot 构建:一个交易服务(Transaction Service)向核心银行服务(Core Banking Service)发送支付指令,同时将客户个人身份信息(PII)和 KYC 数据写入 PostgreSQL。

相关赞助商

此外,该平台还会有贷款协议、开户文件归档在 Amazon S3 中,以及接入 SWIFT 和 ACH 连接器的监管报告流水线所依赖的 OAuth2 服务账户令牌。这种拓扑对于中大型银行来说非常典型。问题是,对于其中每一部分,“抗量子安全”到底意味着什么。答案因部分而异。

本文针对该拓扑给出了四个具体模式,使用的是名为 PqcStarterLib 的 Spring Boot PQC 库。它把 Bouncy Castle 的 PQC Provider 封装在三个自动配置的 Bean 后面。这些模式涵盖:银行内部服务之间的负载加密;在 Jakarta Persistence 写入数据库之前对 PII 和 KYC 字段进行字段级加密;使用 Dilithium 对贷款协议和审计记录进行长期文档签名;以及为核心银行和监管流水线中使用的服务账户提供抗量子 OAuth2 令牌签名。每个模式都附带可运行的 Spring Boot 代码,并诚实指出是什么阻碍了它直接进入生产环境。

零售银行之所以是“现在收集,以后解密”(HNDL)特别有吸引力的目标,是因为银行数据的保质期很长。今天被盗的客户 SSN 在 2035 年仍然有用。一份贷款协议,其 RSA 签名在十年后会被伪造,这是一种无法追溯补救的法律责任。本文中的模式就是围绕这一现实设计的。

威胁的通俗解释

RSA 和 ECDSA 之所以有效,是因为分解大整数和求解离散对数对于经典计算机来说是难题。而运行 Shor 算法 的量子计算机可以在多项式时间内解决这两个问题。IBM、谷歌以及多个国家实验室目前已经拥有可工作的量子处理器,只是还没有达到破解 RSA-2048 所需的规模。多数专家认为这一临界点大约在 2030 到 2035 年之间。

等不起的部分是 HNDL。攻击者现在就在拦截并存储加密的 TLS 流量。建立 TLS 会话时使用的 RSA 密钥交换会与密文一起被记录下来。一旦存在足够强大的量子计算机,他们就会回头解密。对零售银行来说,受影响范围包括服务之间流动的客户 KYC 数据、交易记录、银行间结算消息,以及任何通过线路传输且需要多年保持机密的文档。

另一个等不起的部分是长期有效的签名文档。如果一份贷款协议今天用 RSA 签署,并且需要在 2036 年仍然具备法律效力,那么你面临的问题在事后无法修复。银行会将贷款协议、开户合同和审计跟踪归档七到三十年不等,具体取决于监管辖区。你无法追溯性地重新签署已归档的文档。

短生命周期数据风险较低。客户会话令牌 15 分钟过期,即使使用 RSA 签名密钥也基本没问题,因为它在任何人破解之前就已经没有价值了。但用于核心银行集成、欺诈检测流水线和 SWIFT 连接器、且有效期为数月乃至更长时间的 OAuth2 服务账户令牌,就另当别论了。这正是 HNDL 攻击的目标。

PqcStarterLib 为 Spring Boot 带来了什么

PqcStarterLib 基于 Bouncy Castle 对 FIPS 203 和 FIPS 204 的实现构建。它暴露了三个在启动时自动配置的 Spring Bean:

  • PqcEncryptionService 提供混合加密:使用 Kyber KEM 建立一次性共享密钥,然后使用 AES-256-GCM 加密实际负载。任何需要保密的服务间消息体或数据库字段,都可以使用此服务。
  • PqcSignatureService 使用 CRYSTALS-Dilithium 提供签名和验证。可用于贷款协议、KYC 文档、审计记录、构建产物以及需要证明内容未被篡改的 OAuth2 令牌。
  • PqcKeyPairGenerator 生成 Kyber 和 Dilithium 密钥对,并作为自动配置的 Spring Bean 存在。

集成接口只有三行代码:

$ java
@Autowired
PqcEncryptionService pqc;
@Autowired
PqcSignatureService pqcSig;
byte[] ciphertext = pqc.encrypt(data, recipientPublicKey).toBytes();
byte[] signature = pqcSig.sign(document, myPrivateKey);
boolean ok = pqcSig.verify(document, signature, myPublicKey);

关于依赖的说明

Bouncy Castle(bcprov-jdk18on)向后兼容到 JDK 11,覆盖了多数处于 LTS 周期内的银行环境。如果你已经在使用 JDK 24 及以上版本,SunJCE Provider 现在原生支持 ML-KEM 和 ML-DSA,分别通过 JEP 496JEP 497 提供,无需外部库:

$ java
// JDK 24+ only, no Bouncy Castle required
KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-KEM-768");
KeyPair kyberPair = kpg.generateKeyPair();
Signature signer = Signature.getInstance("ML-DSA-65");
signer.initSign(dilithiumPrivateKey);
signer.update(message);
byte[] sig = signer.sign();

Bouncy Castle 提供更多参数集灵活性,并且支持 JDK 11 和 JDK 17。而原生 Provider 零依赖,也是符合 NIST 标准的 Java 工具链正在汇聚的方向。如果你的银行本来就在规划 JDK 24 升级,原生路径值得采用。

用例 1:服务间负载加密

现状

交易服务通过 HTTP 向核心银行服务发送客户支付指令。TLS 保护了传输链路,但无法抵御 HNDL。攻击者今天记录下 RSA 密钥交换,将来可以用量子计算机解密整个会话。在典型的银行基础设施中,TLS 通常还会在 API 网关、服务网格和内部负载均衡器处终止,因此交易服务与核心银行服务之间的实际微服务跳转,在内部边界内往往根本未加密。

模式

在发送前,使用 Kyber+AES-256-GCM 对 HTTP 请求体进行加密,这与 TLS 无关。核心银行服务收到 PqcEncryptedPayload 记录后,用其 Kyber 私钥解密。即使完全去掉 TLS,支付指令也仍然受到保护。

$ java
// Transaction Service: sender
@Autowired
PqcEncryptionService pqc;
PaymentInstruction instruction = buildInstruction(transfer);
byte[] payload = objectMapper.writeValueAsBytes(instruction);
PqcEncryptedPayload encrypted = pqc.encrypt(
        payload,
        coreBankingPublicKey // Kyber-768 public key
);
restTemplate.postForObject("/core-banking/process", encrypted, Void.class);

// Core Banking Service: receiver
@PostMapping("/core-banking/process")
public ResponseEntity process(@RequestBody PqcEncryptedPayload body) {
    byte[] plaintext = pqc.decrypt(body, coreBankingPrivateKey);
    PaymentInstruction instruction = objectMapper.readValue(plaintext, PaymentInstruction.class);
    // post to ledger...
    return ResponseEntity.ok().build();
}

这正是 NIST 在其迁移指南中描述的双层模式:TLS 应对经典攻击者,Kyber 应对量子攻击者。两者必须被独立攻破。你不需要改动 TLS 设置、API 网关配置或服务网格。

注意事项

Kyber-768 公钥约为 1,184 字节。这个大小放在 HTTP 请求体中没问题,但不要放在请求头中。Dilithium-3 签名约为 3,300 字节,如果你要将其序列化到 JWT 声明或 ISO 20022 消息信封中,这一点很重要。

用例 2:PII 和 KYC 字段级加密

现状

客户 SSN、税务识别号、出生日期字段和 KYC 文档引用通常存放在 PostgreSQL 中。它们可能在静态存储时经过 AES 加密,但密钥通常放在环境变量中,或在启动时从配置服务器拉取。一旦因 SQL 注入导致数据库被导出、备份配置错误或内部人员泄露,所有数据都会暴露。在零售银行中,仅是一份 SSN 泄露,就可能构成 GDPR、CCPA、GLBA 以及各州金融数据保护法律下的监管违规。

模式

在使用 Jakarta Persistence 持久化实体之前,用 PqcEncryptionService 对每个敏感字段进行加密。列中存储的是 Base64 编码的密文块。明文值使用 @Transient 注解标注,永远不会写入数据库。

$ java
// Save
public CustomerProfile saveProfile(CustomerDto dto) {
    CustomerProfile profile = new CustomerProfile();
    profile.setFullName(dto.getFullName());
    byte[] ssnCipher = pqc.encryptToBytes(
            dto.getSsn().getBytes(UTF_8),
            masterKeyPair.getPublic()
    );
    profile.setEncryptedSsn(
            Base64.getEncoder().encodeToString(ssnCipher)
    );
    return repo.save(profile);
}

// Retrieve
public String getSsn(Long customerId) {
    CustomerProfile p = repo.findById(customerId).orElseThrow();
    byte[] blob = Base64.getDecoder().decode(p.getEncryptedSsn());
    return new String(pqc.decrypt(blob, masterKeyPair.getPrivate()), UTF_8);
}

阻碍字段级加密进入生产环境的密钥管理问题

上面的代码在开发环境中运行良好,但 masterKeyPair.getPrivate() 是放在 JVM 堆中的 Kyber 私钥。在银行语境下,这种做法会带来三个问题,并且无法通过任何安全审计:

  • 服务器重启时如果没有持久化密钥库,所有加密的客户记录将永久不可读。
  • 任意运行实例的堆转储都会暴露数据库中所有 SSN 和税号的主私钥。
  • 没有密钥轮换,而 GLBA、PCI-DSS 和 SOC 2 都明确要求这一点。

解决办法是将所有 Kyber 密钥操作都通过 AWS KMS 或 HashiCorp Vault 进行。应用程序永远不持有原始私钥。每次解密都是一次有日志、可审计的 KMS 调用,并按计划进行密钥轮换。这种模式已被充分理解,但它本身是独立于加密功能的一项工程工作,必须在任何加密字段接近生产环境之前完成。否则,你所拥有的只是可用的概念验证,而不是可部署的系统。

用例 3:贷款协议和审计跟踪的文档签名

现状

零售银行每天都会签署贷款协议、开户合同和监管审计记录。这些文档归档期长达七到三十年。今天用 RSA 签署的贷款协议,到 2035 年左右签名将可以被伪造。银行如果在 2034 年才发现这一风险,将无法追溯性地重新签署过去十年的归档文档。这在法律风险和合规层面都是问题,例如欧盟的 DORA 和美国的 OCC 指引。

模式

在创建时就用 Dilithium 为每份长期文档签名。Dilithium 是一种基于格的算法,其安全假设不受 Shor 算法影响。今天创建的签名到 2045 年在密码学上仍然有效。

$ java
@Entity
public class CustomerProfile {
    private String fullName;
    private String accountNumber;
    @Column(name = "ssn_encrypted")
    private String encryptedSsn;
    @Column(name = "tax_id_encrypted")
    private String encryptedTaxId;
    @Transient // never persisted
    private String ssn;
    @Transient // never persisted
    private String taxId;
}

// At document creation
public SignedDocument signLoanAgreement(
        byte[] agreementPdf,
        PrivateKey signerKey,
        String officerId) {
    byte[] signature = pqcSig.sign(agreementPdf, signerKey);
    return SignedDocument.builder()
            .document(agreementPdf)
            .signature(Base64.getEncoder().encodeToString(signature))
            .algorithm("DILITHIUM3")
            .signedAt(Instant.now())
            .signerId(officerId)
            .documentType("LOAN_AGREEMENT")
            .build();
}

// Verification: same code works in 2026, 2031, or 2045
public VerificationResult verify(SignedDocument doc) {
    byte[] sig = Base64.getDecoder().decode(doc.getSignature());
    PublicKey pub = keyRegistry.getPublicKey(doc.getSignerId());
    boolean valid = pqcSig.verify(doc.getDocument(), sig, pub);
    return VerificationResult.builder()
            .valid(valid)
            .signerId(doc.getSignerId())
            .signedAt(doc.getSignedAt())
            .quantumSafe(true)
            .build();
}

同样的模式也适用于 CI/CD 产物签名。部署到银行基础设施中的每个 JAR,都应该由构建流水线使用 Dilithium 签名。部署门禁会在执行任何 kubectl apply 命令之前验证签名。如果签名不匹配,部署中止并抛出 SecurityException。这可以发现产物仓库与生产环境之间的供应链注入;标准 TLS 无法检测到这一点,因为它发生在仓库边界内部。

在本文的四个模式中,文档和产物签名是最接近生产就绪的。它不依赖 KMS 先就位,签名密钥可以通过现有密钥基础设施管理,而且紧迫性论据足够具体,能够获得法律和合规团队的批准。

用例 4:核心银行服务的抗量子 OAuth2 令牌

在零售银行中,授权层比典型微服务场景更需要紧迫性。原因如下。短期客户会话令牌(RS256,15 分钟过期)确实是低优先级。令牌在任何人破解之前就已经没有价值。但银行场景中有两类令牌是另一回事:用于核心银行集成、欺诈检测引擎、SWIFT 网关连接器和 ACH 报告流水线的 OAuth2 服务账户令牌。这些令牌通常有效期长达数月或数年,存放在 CI/CD 保险库和基础设施自动化工具中。任何带有长期监管意义声明的令牌都一样。这些正是 HNDL 攻击所收集的目标。你的 SWIFT 连接器服务账户令牌,如果今天被窃取并存储,到 2031 年被解密,攻击者将来就能利用经过身份验证的访问权限,重放请求到你的核心银行 API。

模式

将服务账户令牌签名从 RS256 替换为 DILITHIUM3。JWT 结构完全相同,只有 alg 头和签名调用发生变化。

$ java
// Auth Server: service account token issuance
public String issueServiceToken(ServiceAccount account) {
    String header = base64url(
            "{\"alg\":\"DILITHIUM3\",\"typ\":\"JWT\"}"
    );
    String payload = base64url(String.format(
            "{\"sub\":\"%s\",\"scope\":\"%s\",\"iat\":%d,\"exp\":%d}",
            account.getClientId(),
            account.getScopes(),
            now(),
            now() + 86400
    ));
    String signingInput = header + "." + payload;
    byte[] sig = pqcSig.sign(
            signingInput.getBytes(UTF_8),
            authServerPrivateKey
    );
    return signingInput + "." + base64url(sig);
}

// API Gateway: token validation
public Claims validateServiceToken(String jwt) {
    String[] parts = jwt.split("\\.");
    String signingInput = parts[0] + "." + parts[1];
    byte[] signature = base64urlDecode(parts[2]);
    boolean valid = pqcSig.verify(
            signingInput.getBytes(UTF_8),
            signature,
            authServerPublicKey
    );
    if (!valid) throw new InvalidTokenException(
            "Dilithium token verification failed"
    );
    return parsePayload(parts[1]);
}

需要规划的事项

Dilithium-3 签名约为 3,300 字节,而 RS256 只有 256 字节。携带这种签名的 JWT 会明显变大。如果你的 API 网关有请求大小限制,或者你的 SWIFT 连接器对消息信封大小有严格要求,在推出此修复之前请检查这些约束。开源生态中针对 OAuth2 流程的完整 Spring Security 集成仍在推进中,因此请将该用例视为近期计划工作,而不是今天就可以上线的功能。

如何安排工作顺序

大多数团队不应试图一次性完成所有工作。在银行环境中,图 1 描述了风险排序:

[LOADING...]

图 1. 面向 Spring Boot 微服务、按风险优先级排序的 PQC 迁移顺序。(来源:作者制作)

立即进行以下修改:

  • 为 Kyber 密钥管理接入 AWS KMS 或 HashiCorp Vault。没有这一步,其他任何工作都不具备生产安全性。
  • 为承载最敏感数据的两到三个服务间流量添加 Kyber+AES-256-GCM 负载加密:任何涉及客户 PII、KYC 记录或支付指令的流量。
  • 将贷款协议和监管文档签名切换到 Dilithium。这一项最有时效性,因为你无法事后修复已归档文档,而且它不需要先有 KMS。

在接下来六到十八个月内进行以下修改:

  • 将负载加密推广到内部服务网格的其余部分。
  • 使用 Jakarta Persistence/Hibernate ORM 字段加密辅助工具,从 KMS 进行按记录密钥派生。
  • 为银行内部基础设施中的 CI/CD 产物添加 Dilithium 签名。
  • 一旦 JEP 527 在即将发布的 JDK 27 中落地,并且你的云服务商支持新的密码套件,就在 TLS 1.3 中启用 PQC。

密钥管理稳定后,为服务账户增加 Dilithium OAuth2 令牌签名的 Spring Security 集成。接着,在 API 网关层为 Core Banking 和监管流水线端点添加支持 PQC 的令牌验证。

要避免的是把短期客户会话令牌作为起点,因为那是最熟悉的授权模式,但也是这个列表中风险最低的一项。从保质期最长、监管暴露最大的部分开始。

总结

零售银行 Spring Boot 服务群的 PQC 迁移不必一次性全部完成。NIST 标准已经定稿,JDK 24 提供了原生支持,Bouncy Castle 则覆盖了仍在使用 JDK 11 或 JDK 17 的团队。最紧迫的工作——内部服务流量的负载加密、长期文档的 Dilithium 签名、以及 PII 字段级加密——现在都可以通过一个库集成和一次专注的迭代来实现。难点不在于密码学本身,而在于底层密钥管理。没有 KMS 或 HashiCorp Vault,字段级加密就只是可运行的概念验证,无法通过银行安全审查。先把基础打牢,其余工作就能逐步推进。

关于作者

[LOADING...]

Pankaj Sharma

Pankaj Sharma 是首席软件工程师兼解决方案架构师,在金融服务领域拥有丰富的技术领导、系统设计和平台工程经验。他专注于大规模分布式系统、云原生应用安全(AppSec)和企业基础设施自动化,多年来一直为复杂、领域驱动的生态系统构建弹性后端架构和默认安全的流水线。他目前正在撰写关于后量子密码学(PQC)迁移模式以及面向处理客户 PII、支付处理和监管文档签名的 Spring Boot 平台的实用实施框架案例研究。Pankaj 通过技术写作和行业思想领导力,积极分享经过实战检验的工程经验与系统设计策略。

显示更多 显示更少