Skip to content

Add ML-KEM support for KRA key recovery#5362

Draft
ladycfu wants to merge 2 commits into
dogtagpki:masterfrom
ladycfu:pqc-kra-recovery
Draft

Add ML-KEM support for KRA key recovery#5362
ladycfu wants to merge 2 commits into
dogtagpki:masterfrom
ladycfu:pqc-kra-recovery

Conversation

@ladycfu
Copy link
Copy Markdown
Contributor

@ladycfu ladycfu commented May 21, 2026

This commit implements ML-KEM decapsulation support for recovering archived private keys from KRA storage, completing the PQC key archival/recovery workflow.
Depends on #5363 (CryptoUtil ML-KEM bits fix)

Changes:

  • StorageKeyUnit.java:

    • Updated unwrap() for PrivateKey recovery to detect ML-KEM storage cert and use CryptoUtil.decapsulateMLKEM() to recover the shared secret from the KEM ciphertext
    • Updated unwrap() for SymmetricKey recovery with same ML-KEM logic
    • Both methods maintain backward compatibility with RSA/EC storage certs by branching based on storage public key algorithm
    • Added comments documenting the SEQUENCE structure semantics for RSA/EC (wrapped session key) vs ML-KEM (KEM ciphertext)
  • RecoveryService.java:

    • Added note that allowEncDecrypt_recovery is not supported for ML-KEM storage at this time (matching archival code pattern)

Recovery workflow:

  1. Read privateKeyData SEQUENCE from KeyRecord
  2. Extract ciphertext (first OCTET STRING) and wrapped key (second)
  3. If storage cert is ML-KEM: decapsulate to get shared secret
  4. If storage cert is RSA/EC: unwrap session key (existing code path)
  5. Unwrap user's private key with shared secret/session key
  6. Create PKCS#12 with recovered key (works for PQC keys)

The PKCS#12 creation code requires no changes as it already works with PQC keys (verified by hsmCompatVerifyServ tool).

IDM-5473
Assisted-By: Claude

This commit implements ML-KEM decapsulation support for recovering
archived private keys from KRA storage, completing the PQC key
archival/recovery workflow.

Changes:

- StorageKeyUnit.java:
  * Updated unwrap() for PrivateKey recovery to detect ML-KEM storage
    cert and use CryptoUtil.decapsulateMLKEM() to recover the shared
    secret from the KEM ciphertext
  * Updated unwrap() for SymmetricKey recovery with same ML-KEM logic
  * Both methods maintain backward compatibility with RSA/EC storage
    certs by branching based on storage public key algorithm
  * Added comments documenting the SEQUENCE structure semantics for
    RSA/EC (wrapped session key) vs ML-KEM (KEM ciphertext)

- RecoveryService.java:
  * Added note that allowEncDecrypt_recovery is not supported for
    ML-KEM storage at this time (matching archival code pattern)

Recovery workflow:
1. Read privateKeyData SEQUENCE from KeyRecord
2. Extract ciphertext (first OCTET STRING) and wrapped key (second)
3. If storage cert is ML-KEM: decapsulate to get shared secret
4. If storage cert is RSA/EC: unwrap session key (existing code path)
5. Unwrap user's private key with shared secret/session key
6. Create PKCS#12 with recovered key (works for PQC keys)

The PKCS#12 creation code requires no changes as it already works
with PQC keys (verified by hsmCompatVerifyServ tool).

IDM-5473
Assisted-By: Claude
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 53024a9f-ae23-4ae4-a772-86970f6a1794

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for ML-KEM (Post-Quantum Cryptography) storage certificates within the Key Recovery Authority (KRA). Key changes include updating StorageKeyUnit to handle ML-KEM decapsulation for both symmetric and private key recovery paths. Review feedback highlights a critical bug where the session key length is passed in bits instead of bytes, suggests making the algorithm string matching more robust to support variations like 'MLKEM', and recommends refactoring the duplicated decapsulation logic into a shared helper method.

Comment on lines +1298 to +1301
sk = CryptoUtil.decapsulateMLKEM(
storagePrivKey,
session,
params.getSkLength());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The params.getSkLength() method returns the key size in bits (e.g., 128 or 256), but CryptoUtil.decapsulateMLKEM() expects the size in bytes. Passing the bit count will result in an incorrect derived key size, causing the subsequent unwrap operation to fail. The value should be divided by 8.

Suggested change
sk = CryptoUtil.decapsulateMLKEM(
storagePrivKey,
session,
params.getSkLength());
sk = CryptoUtil.decapsulateMLKEM(
storagePrivKey,
session,
params.getSkLength() / 8);

Comment on lines +1354 to +1357
sk = CryptoUtil.decapsulateMLKEM(
storagePrivKey,
session,
params.getSkLength());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The params.getSkLength() method returns the key size in bits (e.g., 128 or 256), but CryptoUtil.decapsulateMLKEM() expects the size in bytes. Passing the bit count will result in an incorrect derived key size, causing the subsequent unwrap operation to fail. The value should be divided by 8.

Suggested change
sk = CryptoUtil.decapsulateMLKEM(
storagePrivKey,
session,
params.getSkLength());
sk = CryptoUtil.decapsulateMLKEM(
storagePrivKey,
session,
params.getSkLength() / 8);

SymmetricKey sk;

// Check if storage cert is ML-KEM (PQC)
if (storageAlg != null && storageAlg.toUpperCase().startsWith("ML-KEM")) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The algorithm check for ML-KEM should be more robust to match the logic used in CryptoUtil.unwrap(). It should also account for the algorithm name "MLKEM" (without the hyphen) to ensure compatibility with different provider naming conventions.

Suggested change
if (storageAlg != null && storageAlg.toUpperCase().startsWith("ML-KEM")) {
if (storageAlg != null && (storageAlg.toUpperCase().startsWith("ML-KEM") || storageAlg.equalsIgnoreCase("MLKEM"))) {

SymmetricKey sk;

// Check if storage cert is ML-KEM (PQC)
if (storageAlg != null && storageAlg.toUpperCase().startsWith("ML-KEM")) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The algorithm check for ML-KEM should be more robust to match the logic used in CryptoUtil.unwrap(). It should also account for the algorithm name "MLKEM" (without the hyphen) to ensure compatibility with different provider naming conventions.

Suggested change
if (storageAlg != null && storageAlg.toUpperCase().startsWith("ML-KEM")) {
if (storageAlg != null && (storageAlg.toUpperCase().startsWith("ML-KEM") || storageAlg.equalsIgnoreCase("MLKEM"))) {

Comment on lines +1290 to +1310
SymmetricKey sk;

// Check if storage cert is ML-KEM (PQC)
if (storageAlg != null && storageAlg.toUpperCase().startsWith("ML-KEM")) {
logger.debug("StorageKeyUnit:unwrap() Using ML-KEM storage cert for symmetric key");

// (1) ML-KEM decapsulation to recover shared secret
PrivateKey storagePrivKey = getPrivateKey();
sk = CryptoUtil.decapsulateMLKEM(
storagePrivKey,
session,
params.getSkLength());

logger.debug("StorageKeyUnit:unwrap() ML-KEM decapsulation complete - recovered shared secret");

} else {
// (1) RSA/EC: unwrap the session key
logger.debug("StorageKeyUnit:unwrap() Using RSA/EC storage cert for symmetric key");
sk = unwrap_session_key(token, session, SymmetricKey.Usage.UNWRAP, params);
logger.debug("StorageKeyUnit:unwrap() session key unwrapped");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for selecting the session key recovery method (ML-KEM decapsulation vs. RSA/EC unwrapping) is duplicated in both unwrap() methods. Consider refactoring this into a private helper method to improve maintainability and ensure consistent behavior across both symmetric and private key recovery paths.

@ladycfu ladycfu force-pushed the pqc-kra-recovery branch from 2f65efa to a9ebe9f Compare May 22, 2026 00:59
Change encapsulateMLKEM() and decapsulateMLKEM() to accept key
size in bits instead of bytes, consistent with PKI convention and
other CryptoUtil methods like generateKey().

The methods now convert bits to bytes internally when calling the
Java KEM API, making them compatible with params.getSkLength()
which returns key size in bits.

IDM-6295
@ladycfu ladycfu force-pushed the pqc-kra-recovery branch from a9ebe9f to 86435c7 Compare May 22, 2026 01:22
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant