Key Takeaways
- If you are already on JDK 24, you can start using the Module-Lattice-Based Key-Encapsulation Mechanism (ML-KEM) and Module-Lattice-Based Digital Signature Algorithm (ML-DSA) through the standard Java Cryptography Extension (JCE) API with no extra library. The JDK upgrade (JDK 24) you were probably already planning also unblocks your entire post-quantum cryptography (PQC) migration.
- Someone is storing your RSA-wrapped TLS sessions right now to decrypt later, so any customer SSNs, transaction records, and Know Your Customer (KYC) documents flowing between your services today are already at risk. Waiting for PQC TLS to arrive at your cloud provider is not a migration plan.
- Encrypting a database field with a Kyber key is not the hard part; rather, the hard part is making sure that key does not live in your JVM heap, because one server restart or one heap dump undoes every encrypted row in your database. Kay Management Services (KMS) or HashiCorp Vault integration needs to come before anything is deployed to production.
- OAuth2 tokens and service account credentials used by core banking, fraud detection, and regulatory reporting pipelines often live for months or years, making them a higher priority for PQC migration than short-lived customer session tokens.
- Start your PQC migration with the data that lives the longest, not with the authorization layer that feels most familiar, because loan agreements and KYC records signed with RSA today will have forgeable signatures around 2035. Therefore, you cannot go back and re-sign archived documents after the fact.
Background
When NIST finalized the FIPS 203 and FIPS 204 specifications in August 2024, most engineering teams in regulated industries started asking the same question: "where do we actually begin?" The obvious answer is "switch to PQC TLS", but that is still rolling out across cloud providers and is something most teams cannot switch on today.
Meanwhile, the actual risk, i.e., adversaries storing your encrypted inter-service traffic right now to decrypt it once quantum hardware catches up, is already in motion.
Consider a standard retail banking microservice platform built on Spring Boot with a Transaction Service that posts payment instructions to a Core Banking Service as well as customer Personally Identifiable Information (PII) and KYC data in PostgreSQL.
Additionally, the platform will have loan agreements, account opening documents archived in Amazon S3, and OAuth2 service account tokens wired into regulatory reporting pipelines for SWIFT and ACH connectors. This topology is completely typical for a mid-to-large bank. The question is what "quantum-safe" actually means for each of those pieces. The answer differs for every piece.
This article works through four concrete patterns for that topology using a Spring Boot PQC library called PqcStarterLib, which wraps a Bouncy Castle PQC provider behind three autoconfigured beans.
The patterns cover payload encryption between internal banking services, PII and KYC field-level encryption before Jakarta Persistence writes to the database, long-lived document signing with Dilithium for loan agreements and audit records, and quantum-safe OAuth2 token signing for service accounts used in Core Banking and regulatory pipelines. Each pattern comes with working Spring Boot code and an honest note on what blocks it from going straight to production.
A retail bank is a particularly attractive Harvest Now, Decrypt Later (HNDL) target because the data has a very long shelf life. A customer SSN stolen today will still be useful in 2035. A loan agreement, whose RSA signature becomes forgeable in ten years, is a legal liability that cannot be fixed retroactively. The patterns in this article are designed around that reality.
The Threat in Plain Terms
RSA and ECDSA work because factoring large numbers and solving discrete logarithms are hard problems for classical computers. A quantum computer running Shor's Algorithm solves both in polynomial time. IBM, Google, and several national labs have working quantum processors today, though none yet at the scale needed to break RSA-2048. Most experts put that crossover somewhere between 2030 and 2035.
The part that cannot wait is HNDL. Adversaries are intercepting and storing encrypted TLS traffic right now. The RSA key exchange that establishes the TLS session is recorded alongside the ciphertext. Once a capable quantum computer exists, they go back and decrypt it. For a retail bank, what is in scope includes customer KYC data flowing between services, transaction records, inter-bank settlement messages, as well as any document transferred over the wire that needs to stay confidential for years.
The other part that cannot wait is long-lived signed documents. If a loan agreement is signed today with RSA and will need to be held up legally in 2036, you have a problem that cannot be fixed after the fact. Banks archive loan agreements, account opening contracts, and audit trails exist for anywhere from seven to thirty years depending on regulatory jurisdiction. You cannot retroactively re-sign archived documents.
Short-lived data are lower risk. A customer session token that expires in fifteen minutes is mostly fine even under an RSA signing key, because it is worthless before anyone can crack it. But OAuth2 service account tokens for Core Banking integrations, Fraud Detection pipelines, and SWIFT connectors that live for months are a different story. Those are exactly what HNDL attacks target.
What PqcStarterLib Adds to Spring Boot
PqcStarterLib is built on Bouncy Castle's implementation of FIPS 203 and FIPS 204. It exposes three Spring beans that autoconfigure on startup:
PqcEncryptionServiceprovides hybrid encryption using Kyber KEM to establish a one-time shared secret, then AES-256-GCM to encrypt the actual payload. Use this service for any inter-service message body or database field that needs to stay confidential.PqcSignatureServiceprovides signing and verification using CRYSTALS-Dilithium. Use this service for loan agreements, KYC documents, audit records, build artifacts, and OAuth2 tokens where you need to prove content has not been altered.PqcKeyPairGeneratorgenerates Kyber and Dilithium key pairs as autoconfigured Spring beans.
The integration surface is three lines of code:
@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);
A Note on Dependencies
Bouncy Castle (bcprov-jdk18on) is backwards compatible to JDK 11, which covers most banking shops on LTS cycles. If you are on JDK 24+, the SunJCE provider now includes ML-KEM and ML-DSA natively via JEP 496 and JEP 497, respectively. There is no external library required:
// 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 gives you more parameter set flexibility and works on JDK 11 and JDK 17. The native provider has zero dependencies and is what NIST-standard Java tooling is converging on. If your bank is planning a JDK 24 upgrade anyway, the native path is worth taking.
Use Case 1: Inter-Service Payload Encryption
The Situation
Transaction Service posts customer payment instructions to Core Banking Service over HTTP. TLS protects the wire, but not against HNDL. An adversary who records the RSA key exchange today can decrypt the full session later with a quantum computer. In typical banking infrastructure, TLS also gets terminated at API gateways, service meshes, and internal load balancers, so the actual microservice hop between Transaction Service and Core Banking is often unencrypted inside the perimeter anyway.
The Pattern
Encrypt the HTTP body with Kyber+AES-256-GCM before sending, independent of TLS. Core Banking Service receives a PqcEncryptedPayload record and decrypts it with its Kyber private key. The payment instruction is protected even if TLS is stripped entirely.
// 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();
}
This is the dual-layer pattern NIST describes in their migration guidance: TLS handles classical attackers, Kyber handles quantum attackers. Both have to be broken independently. You do not touch your TLS setup, your API gateway config, or your service mesh.
What to Watch For
Kyber-768 public keys are around 1,184 bytes. That size is fine in an HTTP body, but do not put them in headers. Dilithium-3 signatures are around 3,300 bytes, which matters if you are serializing them into JWT claims or ISO 20022 message envelopes.
Use Case 2: PII and KYC Field-Level Encryption
The Situation
Customer SSNs, tax identification numbers, date-of-birth fields, and KYC document references typically sit in PostgreSQL. They may be AES-encrypted at rest, but the key is usually in an environment variable or pulled from a configuration server at startup. A database dump via SQL injection, a misconfigured backup, or an insider exposes everything. In retail banking, a single SSN dump is a regulatory breach under GDPR, CCPA, GLBA, and state-level financial data protection laws.
The Pattern
Encrypt each sensitive field with the PqcEncryptionService class before the entity is persisted with Jakarta Persistence. The column stores a base64-encoded ciphertext blob. The plaintext value is annotated with @Transient and never touches the database.
// 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);
}
The Key Management Problem That Blocks Field-Level Encryption From Reaching Production
The code above works fine in a development environment, but masterKeyPair.getPrivate() is a Kyber private key sitting in the JVM heap. In a banking context, that approach creates three problems that will fail any security audit:
- A server restart with no persistent key store makes every encrypted customer record permanently unreadable.
- A heap dump from any running instance exposes the master private key for every SSN and tax ID in the database.
- There is no key rotation, which GLBA, PCI-DSS, and SOC 2 all explicitly require.
The fix is routing all Kyber key operations through AWS KMS or HashiCorp Vault. The application never holds the raw private key. Every decrypt is a logged, auditable KMS call with key rotation on a schedule. This pattern is well-understood, but is a separate engineering workstream from the encryption itself and needs to be in place before any encrypted fields go anywhere near production. Without it, what you have is a working proof of concept, not a deployable system.
Use Case 3: Document Signing for Loan Agreements and Audit Trails
The Situation
Retail banks sign loan agreements, account opening contracts, and regulatory audit records daily. These documents are archived for seven to thirty years. A loan agreement signed today with RSA will have a forgeable signature around 2035. A bank that discovers this liability in 2034 would not be able to retroactively re-sign ten years of archived documents. This issue is both a legal risk and a regulatory compliance problem under frameworks like DORA in the EU and OCC guidelines in the US.
The Pattern
Sign every long-lived document with Dilithium at creation time. Dilithium is a lattice-based algorithm whose security assumptions are not affected by Shor's Algorithm. A signature created today will still be cryptographically valid in 2045.
@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();
}
The same pattern applies to continuous integration and continuous delivery (CI/CD) artifact signing. Every JAR deployed into banking infrastructure should be signed with Dilithium by the build pipeline. The deploy gate verifies the signature before executing any kubectl apply commands. If the signature does not match, deploy aborts with a SecurityException. This solution catches supply chain injection between the artifact registry and production, which standard TLS cannot detect because it happens inside the registry boundary.
Of the four patterns in this article, document and artifact signing is the one closest to being production-ready. The pattern does not depend on KMS being in place, the signing key can be managed through existing key infrastructure, and the urgency argument is concrete enough to get sign-off from legal and compliance teams.
Use Case 4: Quantum-Safe OAuth2 Tokens for Core Banking Services
In retail banking, the authorization layer deserves more urgency than in a typical microservice shop. Here is why.
Short-lived customer session tokens (RS256, fifteen minute expiry) are genuinely low priority. The token is worthless before anyone can crack it. But two token types in a banking context are a different story OAuth2 service account tokens for Core Banking integrations, fraud detection engines, SWIFT gateway connectors, and ACH reporting pipelines. These commonly live for months or years and sit in CI/CD vaults and infrastructure automation tools. Any token carrying claims with regulatory significance over a long period.
These are exactly the tokens that HNDL attacks collect. A service account token for your SWIFT connector, stolen and stored today, to be decrypted in 2031, would allow an attacker future authenticated access to replay against your Core Banking API.
The Pattern
Replace RS256 with DILITHIUM3 for service account token signing. The JWT structure is identical, only the alg header and the signing call change.
// 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]);
}
What to Plan For
Dilithium-3 signatures are around 3,300 bytes versus 256 bytes for RS256. A JWT carrying one is noticeably larger. If your API gateway enforces request size limits or your SWIFT connector has strict message envelope sizes, check those constraints before rolling out this fix. Full Spring Security integration for the OAuth2 flow is still in progress in the open source ecosystem, so treat this use case as near-term planned work rather than something you can ship today.
How to Sequence the Work
Most teams should not try to do all of this work at once. In a banking context, Figure 1 describes the risk ordering:

Figure 1. A risk-prioritized PQC migration sequence for Spring Boot microservices. (Source: created by the author).
Make these changes right now:
- Get AWS KMS or HashiCorp Vault in place for Kyber key management. Nothing else is production-safe without this change.
- Add Kyber+AES-256-GCM payload encryption to the two or three service-to-service flows that carry the most sensitive data: anything touching customer PII, KYC records, or payment instructions.
- Switch loan agreement and regulatory document signing to Dilithium. This item is the most time-sensitive because you cannot retroactively fix archived documents and it does not require KMS to be in place first.
Make these changes In the next six to eighteen months:
- Roll payload encryption across the rest of the internal service mesh.
- Jakarta Persistence/Hibernate ORM field encryption helpers with per-record key derivation from KMS.
- Dilithium signing for CI/CD artifacts across internal banking infrastructure.
- PQC in TLS 1.3 once JEP 527 lands in the upcoming release of JDK 27 and your cloud provider supports the new cipher suites.
After key management is solid add Spring Security integration for Dilithium OAuth2 token signing on service accounts. Next, add PQC-aware token validation at the API gateway layer for Core Banking and regulatory pipeline endpoints.
The thing to avoid is treating short-lived customer session tokens as the starting point because they are the most familiar authorization pattern. They are also the lowest risk item on this list. Start with what has the longest shelf life and the biggest regulatory exposure.
Summary
The PQC migration for a retail banking Spring Boot fleet does not have to happen all at once. The NIST standards are final, JDK 24 has native support, and Bouncy Castle covers teams still on JDK 11 or JDK 17. The most urgent work, payload encryption on internal service traffic, Dilithium signing for long-lived documents, and field-level encryption for PII, is all achievable today with a library integration and a focused sprint.
The hard part is not the cryptography; it is the key management underneath. Without KMS or HashiCorp Vault in place, field-level encryption is a working proof of concept that will not pass a banking security review. Get that foundation right first and the rest follows incrementally.