From 0a94e3ef1a0e694d8298dde3415803622e67ff67 Mon Sep 17 00:00:00 2001 From: "hh0328.yu" Date: Tue, 11 Mar 2025 15:17:26 +0900 Subject: [PATCH 1/2] add JcaTlsMLDSASigner --- .../jsse/provider/ProvX509KeyManager.java | 8 + .../jsse/provider/SignatureSchemeInfo.java | 6 +- .../tls/crypto/impl/bc/BcTlsCrypto.java | 3 +- .../JcaDefaultTlsCredentialedSigner.java | 6 + .../crypto/impl/jcajce/JcaTlsCertificate.java | 6 + .../tls/crypto/impl/jcajce/JcaTlsCrypto.java | 3 +- .../crypto/impl/jcajce/JcaTlsMLDSASigner.java | 45 ++++ .../jsse/provider/test/AllTests.java | 3 + .../provider/test/MLDSACredentialsTest.java | 221 ++++++++++++++++++ .../jsse/provider/test/TestUtils.java | 27 ++- 10 files changed, 322 insertions(+), 6 deletions(-) create mode 100644 tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsMLDSASigner.java create mode 100644 tls/src/test/java/org/bouncycastle/jsse/provider/test/MLDSACredentialsTest.java diff --git a/tls/src/main/java/org/bouncycastle/jsse/provider/ProvX509KeyManager.java b/tls/src/main/java/org/bouncycastle/jsse/provider/ProvX509KeyManager.java index 23ec3438aa..f145ac9eab 100644 --- a/tls/src/main/java/org/bouncycastle/jsse/provider/ProvX509KeyManager.java +++ b/tls/src/main/java/org/bouncycastle/jsse/provider/ProvX509KeyManager.java @@ -173,6 +173,10 @@ private static Map createFiltersClient() addFilter(filters, DSAPublicKey.class, "DSA"); addFilter(filters, ECPublicKey.class, "EC"); + addFilter((filters), "ML-DSA-44"); + addFilter((filters), "ML-DSA-65"); + addFilter((filters), "ML-DSA-87"); + return Collections.unmodifiableMap(filters); } @@ -201,6 +205,10 @@ private static Map createFiltersServer() KeyExchangeAlgorithm.SRP_RSA); addFilterLegacyServer(filters, ProvAlgorithmChecker.KU_KEY_ENCIPHERMENT, "RSA", KeyExchangeAlgorithm.RSA); + addFilter((filters), "ML-DSA-44"); + addFilter((filters), "ML-DSA-65"); + addFilter((filters), "ML-DSA-87"); + return Collections.unmodifiableMap(filters); } diff --git a/tls/src/main/java/org/bouncycastle/jsse/provider/SignatureSchemeInfo.java b/tls/src/main/java/org/bouncycastle/jsse/provider/SignatureSchemeInfo.java index e56f8bc677..6755c13718 100644 --- a/tls/src/main/java/org/bouncycastle/jsse/provider/SignatureSchemeInfo.java +++ b/tls/src/main/java/org/bouncycastle/jsse/provider/SignatureSchemeInfo.java @@ -72,9 +72,9 @@ private enum All sm2sig_sm3(SignatureScheme.sm2sig_sm3, "SM3withSM2", "EC"), // TODO[tls] Need mechanism for restricting signature schemes to TLS 1.3+ before adding -// DRAFT_mldsa44(SignatureScheme.DRAFT_mldsa44, "ML-DSA-44", "ML-DSA-44"), -// DRAFT_mldsa65(SignatureScheme.DRAFT_mldsa65, "ML-DSA-65", "ML-DSA-65"), -// DRAFT_mldsa87(SignatureScheme.DRAFT_mldsa87, "ML-DSA-87", "ML-DSA-87"), + DRAFT_mldsa44(SignatureScheme.DRAFT_mldsa44, "ML-DSA-44", "ML-DSA-44"), + DRAFT_mldsa65(SignatureScheme.DRAFT_mldsa65, "ML-DSA-65", "ML-DSA-65"), + DRAFT_mldsa87(SignatureScheme.DRAFT_mldsa87, "ML-DSA-87", "ML-DSA-87"), /* * Legacy/Historical: mostly not supported in 1.3, except ecdsa_sha1 and rsa_pkcs1_sha1 are diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsCrypto.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsCrypto.java index 6265353d9e..6d02c2fc90 100644 --- a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsCrypto.java +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsCrypto.java @@ -455,11 +455,12 @@ public boolean hasSignatureScheme(int signatureScheme) switch (signatureScheme) { case SignatureScheme.sm2sig_sm3: + return false; // TODO[tls] Test coverage before adding case SignatureScheme.DRAFT_mldsa44: case SignatureScheme.DRAFT_mldsa65: case SignatureScheme.DRAFT_mldsa87: - return false; + return true; default: { short signature = SignatureScheme.getSignatureAlgorithm(signatureScheme); diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaDefaultTlsCredentialedSigner.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaDefaultTlsCredentialedSigner.java index bea3fed4a2..aac3ae8e0b 100644 --- a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaDefaultTlsCredentialedSigner.java +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaDefaultTlsCredentialedSigner.java @@ -66,6 +66,12 @@ else if ("Ed448".equalsIgnoreCase(algorithm)) { signer = new JcaTlsEd448Signer(crypto, privateKey); } + else if ("ML-DSA-44".equalsIgnoreCase(algorithm) + || "ML-DSA-65".equalsIgnoreCase(algorithm) + || "ML-DSA-87".equalsIgnoreCase(algorithm)) + { + signer = new JcaTlsMLDSASigner(crypto, privateKey); + } else { throw new IllegalArgumentException("'privateKey' type not supported: " + privateKey.getClass().getName()); diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsCertificate.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsCertificate.java index 9f14399762..56034291ff 100644 --- a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsCertificate.java +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsCertificate.java @@ -277,6 +277,7 @@ public Tls13Verifier createVerifier(int signatureScheme) throws IOException case SignatureScheme.DRAFT_mldsa44: case SignatureScheme.DRAFT_mldsa65: case SignatureScheme.DRAFT_mldsa87: + return crypto.createTls13Verifier("ML-DSA", null, getPubKeyMLDSA()); default: throw new TlsFatalAlert(AlertDescription.internal_error); @@ -396,6 +397,11 @@ PublicKey getPubKeyRSA() throws IOException return getPublicKey(); } + PublicKey getPubKeyMLDSA() throws IOException + { + return getPublicKey(); + } + public short getLegacySignatureAlgorithm() throws IOException { PublicKey publicKey = getPublicKey(); diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsCrypto.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsCrypto.java index a42f4e201f..1685bc2d9d 100644 --- a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsCrypto.java +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsCrypto.java @@ -778,11 +778,12 @@ public boolean hasSignatureScheme(int signatureScheme) switch (signatureScheme) { case SignatureScheme.sm2sig_sm3: + return false; // TODO[tls] Implement before adding case SignatureScheme.DRAFT_mldsa44: case SignatureScheme.DRAFT_mldsa65: case SignatureScheme.DRAFT_mldsa87: - return false; + return true; default: { short signature = SignatureScheme.getSignatureAlgorithm(signatureScheme); diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsMLDSASigner.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsMLDSASigner.java new file mode 100644 index 0000000000..2e40e274ed --- /dev/null +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsMLDSASigner.java @@ -0,0 +1,45 @@ +package org.bouncycastle.tls.crypto.impl.jcajce; + +import java.io.IOException; +import java.security.PrivateKey; + +import org.bouncycastle.tls.SignatureAndHashAlgorithm; +import org.bouncycastle.tls.crypto.TlsSigner; +import org.bouncycastle.tls.crypto.TlsStreamSigner; + +public class JcaTlsMLDSASigner + implements TlsSigner +{ + protected final JcaTlsCrypto crypto; + protected final PrivateKey privateKey; + + public JcaTlsMLDSASigner(JcaTlsCrypto crypto, PrivateKey privateKey) + { + if (null == crypto) + { + throw new NullPointerException("crypto"); + } + if (null == privateKey) + { + throw new NullPointerException("privateKey"); + } + + this.crypto = crypto; + this.privateKey = privateKey; + } + + public byte[] generateRawSignature(SignatureAndHashAlgorithm algorithm, byte[] hash) throws IOException + { + throw new UnsupportedOperationException(); + } + + public TlsStreamSigner getStreamSigner(SignatureAndHashAlgorithm algorithm) throws IOException + { + if (algorithm == null) + { + throw new IllegalStateException("Invalid algorithm: " + algorithm); + } + + return crypto.createStreamSigner("ML-DSA", null, privateKey, false); + } +} diff --git a/tls/src/test/java/org/bouncycastle/jsse/provider/test/AllTests.java b/tls/src/test/java/org/bouncycastle/jsse/provider/test/AllTests.java index 3207896b59..012190b1e8 100644 --- a/tls/src/test/java/org/bouncycastle/jsse/provider/test/AllTests.java +++ b/tls/src/test/java/org/bouncycastle/jsse/provider/test/AllTests.java @@ -4,6 +4,8 @@ import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; +import test.java.org.bouncycastle.jsse.provider.test.MLDSACredentialsTest; + import org.bouncycastle.test.PrintTestResult; public class AllTests @@ -25,6 +27,7 @@ public static Test suite() suite.addTestSuite(ConfigTest.class); suite.addTestSuite(ECDSACredentialsTest.class); suite.addTestSuite(EdDSACredentialsTest.class); + suite.addTestSuite(MLDSACredentialsTest.class); suite.addTestSuite(InstanceTest.class); suite.addTestSuite(KeyManagerFactoryTest.class); suite.addTestSuite(PSSCredentialsTest.class); diff --git a/tls/src/test/java/org/bouncycastle/jsse/provider/test/MLDSACredentialsTest.java b/tls/src/test/java/org/bouncycastle/jsse/provider/test/MLDSACredentialsTest.java new file mode 100644 index 0000000000..01a66ebaca --- /dev/null +++ b/tls/src/test/java/org/bouncycastle/jsse/provider/test/MLDSACredentialsTest.java @@ -0,0 +1,221 @@ +package org.bouncycastle.jsse.provider.test; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.concurrent.CountDownLatch; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; + +import junit.framework.TestCase; + +public class MLDSACredentialsTest + extends TestCase +{ + protected void setUp() + { + ProviderUtils.setupLowPriority(false); + } + + private static final String HOST = "localhost"; + private static final int PORT_NO_13_MLDSA44 = 9050; + private static final int PORT_NO_13_MLDSA65 = 9051; + private static final int PORT_NO_13_MLDSA87 = 9052; + + static class MLDSAClient + implements TestProtocolUtil.BlockingCallable + { + private final int port; + private final String protocol; + private final KeyStore trustStore; + private final KeyStore clientStore; + private final char[] clientKeyPass; + private final CountDownLatch latch; + + MLDSAClient(int port, String protocol, KeyStore clientStore, char[] clientKeyPass, + X509Certificate trustAnchor) throws GeneralSecurityException, IOException + { + KeyStore trustStore = createKeyStore(); + trustStore.setCertificateEntry("server", trustAnchor); + + this.port = port; + this.protocol = protocol; + this.trustStore = trustStore; + this.clientStore = clientStore; + this.clientKeyPass = clientKeyPass; + this.latch = new CountDownLatch(1); + } + + public Exception call() throws Exception + { + try + { + TrustManagerFactory trustMgrFact = TrustManagerFactory.getInstance("PKIX", + ProviderUtils.PROVIDER_NAME_BCJSSE); + trustMgrFact.init(trustStore); + + KeyManagerFactory keyMgrFact = KeyManagerFactory.getInstance("PKIX", + ProviderUtils.PROVIDER_NAME_BCJSSE); + keyMgrFact.init(clientStore, clientKeyPass); + + SSLContext clientContext = SSLContext.getInstance("TLS", ProviderUtils.PROVIDER_NAME_BCJSSE); + clientContext.init(keyMgrFact.getKeyManagers(), trustMgrFact.getTrustManagers(), + SecureRandom.getInstance("DEFAULT", ProviderUtils.PROVIDER_NAME_BC)); + + SSLSocketFactory fact = clientContext.getSocketFactory(); + SSLSocket cSock = (SSLSocket)fact.createSocket(HOST, port); + cSock.setEnabledProtocols(new String[]{ protocol }); + + SSLSession session = cSock.getSession(); + assertNotNull(session); + assertFalse("SSL_NULL_WITH_NULL_NULL".equals(session.getCipherSuite())); + assertEquals("CN=Test CA Certificate", session.getLocalPrincipal().getName()); + assertEquals("CN=Test CA Certificate", session.getPeerPrincipal().getName()); + + TestProtocolUtil.doClientProtocol(cSock, "Hello"); + } + finally + { + latch.countDown(); + } + + return null; + } + + public void await() + throws InterruptedException + { + latch.await(); + } + } + + static class MLDSAServer + implements TestProtocolUtil.BlockingCallable + { + private final int port; + private final String protocol; + private final KeyStore serverStore; + private final char[] keyPass; + private final KeyStore trustStore; + private final CountDownLatch latch; + + MLDSAServer(int port, String protocol, KeyStore serverStore, char[] keyPass, X509Certificate trustAnchor) + throws GeneralSecurityException, IOException + { + KeyStore trustStore = createKeyStore(); + trustStore.setCertificateEntry("client", trustAnchor); + + this.port = port; + this.protocol = protocol; + this.serverStore = serverStore; + this.keyPass = keyPass; + this.trustStore = trustStore; + this.latch = new CountDownLatch(1); + } + + public Exception call() throws Exception + { + try + { + KeyManagerFactory keyMgrFact = KeyManagerFactory.getInstance("PKIX", + ProviderUtils.PROVIDER_NAME_BCJSSE); + keyMgrFact.init(serverStore, keyPass); + + TrustManagerFactory trustMgrFact = TrustManagerFactory.getInstance("PKIX", + ProviderUtils.PROVIDER_NAME_BCJSSE); + trustMgrFact.init(trustStore); + + SSLContext serverContext = SSLContext.getInstance("TLS", ProviderUtils.PROVIDER_NAME_BCJSSE); + serverContext.init(keyMgrFact.getKeyManagers(), trustMgrFact.getTrustManagers(), + SecureRandom.getInstance("DEFAULT", ProviderUtils.PROVIDER_NAME_BC)); + + SSLServerSocketFactory fact = serverContext.getServerSocketFactory(); + SSLServerSocket sSock = (SSLServerSocket)fact.createServerSocket(port); + + SSLUtils.enableAll(sSock); + sSock.setNeedClientAuth(true); + + latch.countDown(); + + SSLSocket sslSock = (SSLSocket)sSock.accept(); + sslSock.setEnabledProtocols(new String[]{ protocol }); + + SSLSession session = sslSock.getSession(); + assertNotNull(session); + assertFalse("SSL_NULL_WITH_NULL_NULL".equals(session.getCipherSuite())); + assertEquals("CN=Test CA Certificate", session.getLocalPrincipal().getName()); + assertEquals("CN=Test CA Certificate", session.getPeerPrincipal().getName()); + + TestProtocolUtil.doServerProtocol(sslSock, "World"); + + sslSock.close(); + sSock.close(); + } + finally + { + latch.countDown(); + } + + return null; + } + + public void await() throws InterruptedException + { + latch.await(); + } + } + + public void test13_MLDSA44() throws Exception + { + implTestMLDSACredentials(PORT_NO_13_MLDSA44, "TLSv1.3", TestUtils.generateMLDSAKeyPair("ML-DSA-44")); + } + + public void test13_MLDSA65() throws Exception + { + implTestMLDSACredentials(PORT_NO_13_MLDSA65, "TLSv1.3", TestUtils.generateMLDSAKeyPair("ML-DSA-65")); + } + + public void test13_MLDSA87() throws Exception + { + implTestMLDSACredentials(PORT_NO_13_MLDSA87, "TLSv1.3", TestUtils.generateMLDSAKeyPair("ML-DSA-87")); + } + + private void implTestMLDSACredentials(int port, String protocol, KeyPair caKeyPair) throws Exception + { + char[] keyPass = "keyPassword".toCharArray(); + + X509Certificate caCert = TestUtils.generateRootCert(caKeyPair); + + KeyStore serverKs = createKeyStore(); + serverKs.setKeyEntry("server", caKeyPair.getPrivate(), keyPass, new X509Certificate[]{ caCert }); + + KeyStore clientKs = createKeyStore(); + clientKs.setKeyEntry("client", caKeyPair.getPrivate(), keyPass, new X509Certificate[]{ caCert }); + + TestProtocolUtil.runClientAndServer(new MLDSAServer(port, protocol, serverKs, keyPass, caCert), + new MLDSAClient(port, protocol, clientKs, keyPass, caCert)); + } + + private static KeyStore createKeyStore() throws GeneralSecurityException, IOException + { + /* + * NOTE: At the time of writing, default JKS implementation can't recover PKCS8 private keys + * with version != 0, which e.g. is the case when a public key is included, which the BC + * provider currently does for MLDSA. + */ +// KeyStore keyStore = KeyStore.getInstance("JKS"); + KeyStore keyStore = KeyStore.getInstance("PKCS12", "BC"); + keyStore.load(null, null); + return keyStore; + } +} diff --git a/tls/src/test/java/org/bouncycastle/jsse/provider/test/TestUtils.java b/tls/src/test/java/org/bouncycastle/jsse/provider/test/TestUtils.java index 05963eb4b6..0da0bfc81b 100644 --- a/tls/src/test/java/org/bouncycastle/jsse/provider/test/TestUtils.java +++ b/tls/src/test/java/org/bouncycastle/jsse/provider/test/TestUtils.java @@ -63,6 +63,7 @@ import org.bouncycastle.asn1.x509.Time; import org.bouncycastle.asn1.x509.V3TBSCertificateGenerator; import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; +import org.bouncycastle.jcajce.spec.MLDSAParameterSpec; import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec; import org.bouncycastle.jsse.BCSSLConnection; import org.bouncycastle.jsse.BCSSLEngine; @@ -103,7 +104,9 @@ private static Map createAlgIDs() algIDs.put("SHA256withECDSA", new AlgorithmIdentifier(X9ObjectIdentifiers.ecdsa_with_SHA256)); algIDs.put("Ed25519", new AlgorithmIdentifier(TestOIDs.id_Ed25519)); algIDs.put("Ed448", new AlgorithmIdentifier(TestOIDs.id_Ed448)); - + algIDs.put("ML-DSA-44", new AlgorithmIdentifier(NISTObjectIdentifiers.id_ml_dsa_44)); + algIDs.put("ML-DSA-65", new AlgorithmIdentifier(NISTObjectIdentifiers.id_ml_dsa_65)); + algIDs.put("ML-DSA-87", new AlgorithmIdentifier(NISTObjectIdentifiers.id_ml_dsa_87)); return Collections.unmodifiableMap(algIDs); } @@ -292,6 +295,16 @@ public static KeyPair generateEd448KeyPair() return kpGen.generateKeyPair(); } + public static KeyPair generateMLDSAKeyPair(String name) + throws Exception + { + KeyPairGenerator kpGen = KeyPairGenerator.getInstance("ML-DSA", ProviderUtils.PROVIDER_NAME_BC); + + kpGen.initialize(MLDSAParameterSpec.fromName(name), RANDOM); + + return kpGen.generateKeyPair(); + } + public static X509Certificate generateRootCert(KeyPair pair) throws Exception { @@ -320,6 +333,18 @@ else if (alg.equals("Ed448")) { return createSelfSignedCert("CN=Test CA Certificate", "Ed448", pair); } + else if (alg.equals("ML-DSA-44")) + { + return createSelfSignedCert("CN=Test CA Certificate", "ML-DSA-44", pair); + } + else if (alg.equals("ML-DSA-65")) + { + return createSelfSignedCert("CN=Test CA Certificate", "ML-DSA-65", pair); + } + else if (alg.equals("ML-DSA-87")) + { + return createSelfSignedCert("CN=Test CA Certificate", "ML-DSA-87", pair); + } else { throw new IllegalArgumentException(); From 9cb6f38a408682227b18dd32b2ee3928c10ab64e Mon Sep 17 00:00:00 2001 From: "hh0328.yu" Date: Tue, 11 Mar 2025 15:51:44 +0900 Subject: [PATCH 2/2] update test --- .../test/java/org/bouncycastle/jsse/provider/test/AllTests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/tls/src/test/java/org/bouncycastle/jsse/provider/test/AllTests.java b/tls/src/test/java/org/bouncycastle/jsse/provider/test/AllTests.java index 012190b1e8..f778a8a6d0 100644 --- a/tls/src/test/java/org/bouncycastle/jsse/provider/test/AllTests.java +++ b/tls/src/test/java/org/bouncycastle/jsse/provider/test/AllTests.java @@ -4,7 +4,6 @@ import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; -import test.java.org.bouncycastle.jsse.provider.test.MLDSACredentialsTest; import org.bouncycastle.test.PrintTestResult;