Skip to content

Add generic ML-DSA KeyFactory and Signature - #1667

Open
jasonkatonica wants to merge 4 commits into
IBM:mainfrom
jasonkatonica:katonica/issue1567/genericmldsa
Open

Add generic ML-DSA KeyFactory and Signature#1667
jasonkatonica wants to merge 4 commits into
IBM:mainfrom
jasonkatonica:katonica/issue1567/genericmldsa

Conversation

@jasonkatonica

@jasonkatonica jasonkatonica commented Jul 22, 2026

Copy link
Copy Markdown
Member

Register a family-level ML-DSA KeyFactory and Signature service so that callers can use the algorithm name ML-DSA without specifying a parameter set, matching the JEP 497 / JDK 24+ API contract.

  • Register PQCKeyFactory$MLDSA and PQCSignatureImpl$MLDSA as the ML-DSA service. Remove the ML-DSA alias from ML-DSA-65 (it was a
    mis-mapping that silently directed all generic lookups to ML-DSA-65).
  • Split the single name field into familyName (returned by getAlgorithm() such as ML-DSA) and paramSetName (the concrete parameter set such as ML-DSA-65). Add getters and setters for these values such that other code can act accordingly.
  • Add tests to BaseTestPQCKeyInterop. These tests cover all three ML-DSA parameter sets via @ParameterizedTest.
  • Add tests to BaseTestPQCKeys. Expand alias coverage to include case variants, OID aliases, and bare OID strings. Added tests for getAlgorithm() family-name correctness for all ML-DSA and ML-KEM aliases
  • Add tests to BaseTestPQCSignature for generic ML-DSA sign/verify tests to all three parameter sets.

Fixes: #1567

Signed-off-by: Jason Katonica katonica@us.ibm.com

Comment thread src/main/java/com/ibm/crypto/plus/provider/MLKEMImpl.java Outdated
Comment thread src/main/java/com/ibm/crypto/plus/provider/MLKEMImpl.java Outdated
Comment thread src/main/java/com/ibm/crypto/plus/provider/MLKEMImpl.java Outdated
Comment thread src/main/java/com/ibm/crypto/plus/provider/MLKEMImpl.java Outdated

@JinhangZhang JinhangZhang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

}

/**
* Returns the family name for a known PQC algorithm, or the param-set name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think the param-set name is ever returned from this method. Am I missing some scenario?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes agreed. I updated the javadoc here to state that we always return the family name.

}

/**
* Returns the family name for a known PQC algorithm, or the param-set name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Similar comment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes agreed. I updated the javadoc here to state that we always return the family name.

// The generic "ML-DSA" instance accepts any ML-DSA parameter-set key.
String keyParam = keyPrivate.getParamSetName();
boolean paramMatches = keyParam.equalsIgnoreCase(this.alg)
|| ("ML-DSA".equals(this.alg) && keyParam.startsWith("ML-DSA"));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this be keyParam.startsWith("ML-DSA-")? Notice the last - in the comparing string.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes agreed its best to compare using the - at the end to ensure its the param name not family name being matched upon. Updated this code to reflect this.


try {
this.signature.initialize(keyPrivate.getPQCKey(), keyPrivate.getAlgorithm().replace('_', '-'));
this.signature.initialize(keyPrivate.getPQCKey(), keyPrivate.getParamSetName().replace('_', '-'));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can the name ever have a _? Do we need the replace here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You are right, getParamSetName() returns an already formatted param set name so i dont think we need to do this. I've removed the .replace('_', '-') call here and in engineInitVerify.

// The generic "ML-DSA" instance accepts any ML-DSA parameter-set key.
String keyParam = keyPublic.getParamSetName();
boolean paramMatches = keyParam.equalsIgnoreCase(this.alg)
|| ("ML-DSA".equals(this.alg) && keyParam.startsWith("ML-DSA"));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Similar comment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes agreed its best to compare using the - at the end to ensure its the param name not family name being matched upon. Updated this code to reflect this.

// The original and new keys are the same
same = Arrays.equals(publicKeyBytesInterop, pub.getEncoded());
assertTrue(same);
assertTrue(same, "Public key bytes differ for " + pqcAlgorithm);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Similar comment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes agreed we should be using assertArrayEquals() through this entire test. I converted all asserts dealing with byte arrays to use this method.

@ParameterizedTest
@CsvSource({"ML-DSA", "ML-DSA-44", "ML-DSA-65", "ML-DSA-87"})
public void testGenericMLDSAKeyFactoryImportsInteropKeys(String paramSetName) throws Exception {
assumeFalse("OpenJCEPlusFIPS".equals(getProviderName()));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need the assumeFalse here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes agreed that these assumes for the OpenJCEPlusFIPS provider are redundant since we currently do not run these with that provider set in our test paramaters. I removed these redundant checks throughout the BaseTestPQCKeyInterop class

// BC private-key encoding differs; only compare against SunJCE
if (getInteropProviderName().equals(Utils.PROVIDER_SunJCE)) {
PrivateKey priv = genericKF.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Bytes));
assertTrue(Arrays.equals(pkcs8Bytes, priv.getEncoded()),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why not assertArrayEquals()?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes agreed we should be using assertArrayEquals() through this entire test. I converted all asserts dealing with byte arrays to use this method.

PublicKey pubInterop = kfInterop.generatePublic(
new X509EncodedKeySpec(keyPairPlus.getPublic().getEncoded()));

assertTrue(Arrays.equals(keyPairPlus.getPublic().getEncoded(), pubInterop.getEncoded()),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Similar comment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes agreed we should be using assertArrayEquals() through this entire test. I converted all asserts dealing with byte arrays to use this method.

System.out.println("FIPS does not support plain keys. Returning to caller.");
return;
}
assumeFalse("OpenJCEPlusFIPS".equals(getProviderName()));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes agreed that these assumes for the OpenJCEPlusFIPS provider are redundant since we currently do not run these with that provider set in our test paramaters. I removed these redundant checks throughout the BaseTestPQCSignatureWithAliases class

Register a family-level `ML-DSA` `KeyFactory` and `Signature` service so
that callers can use the algorithm name `ML-DSA` without specifying a
parameter set, matching the JEP 497 / JDK 24+ API contract.

- Register `PQCKeyFactory$MLDSA` and `PQCSignatureImpl$MLDSA` as the
`ML-DSA` service. Remove the `ML-DSA` alias from `ML-DSA-65` (it was a
  mis-mapping that silently directed all generic lookups to ML-DSA-65).
- Split the single `name` field into `familyName` (returned by
`getAlgorithm()` such as `ML-DSA`) and `paramSetName` (the concrete
parameter set such as `ML-DSA-65`). Add getters and setters for these
values such that other code can act accordingly.
- Add tests to `BaseTestPQCKeyInterop`. These tests cover all three
`ML-DSA` parameter sets via `@ParameterizedTest`.
- Add tests to `BaseTestPQCKeys`. Expand alias coverage to include case
variants, OID aliases, and bare OID strings. Added tests for
`getAlgorithm()` family-name correctness for all ML-DSA and ML-KEM
aliases
- Add tests to `BaseTestPQCSignature` for generic `ML-DSA` sign/verify
tests to all three parameter sets.

Fixes: IBM#1567

Signed-off-by: Jason Katonica <katonica@us.ibm.com>
@jasonkatonica
jasonkatonica force-pushed the katonica/issue1567/genericmldsa branch from bb7ac47 to 9427647 Compare September 1, 2026 13:03
Signed-off-by: Jason Katonica <katonica@us.ibm.com>
Signed-off-by: Jason Katonica <katonica@us.ibm.com>
@jasonkatonica
jasonkatonica force-pushed the katonica/issue1567/genericmldsa branch 3 times, most recently from 5b0d00a to a6e6bdd Compare September 1, 2026 15:26
Signed-off-by: Jason Katonica <katonica@us.ibm.com>
@jasonkatonica
jasonkatonica force-pushed the katonica/issue1567/genericmldsa branch from a6e6bdd to f391d23 Compare September 1, 2026 17:33
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.

Support generic ML-DSA

4 participants