diff --git a/src/java.base/share/classes/sun/security/provider/NamedKeyPairGenerator.java b/src/java.base/share/classes/sun/security/provider/NamedKeyPairGenerator.java index 651fa80d6f2..310a7ea68d8 100644 --- a/src/java.base/share/classes/sun/security/provider/NamedKeyPairGenerator.java +++ b/src/java.base/share/classes/sun/security/provider/NamedKeyPairGenerator.java @@ -97,7 +97,7 @@ import java.security.spec.NamedParameterSpec; /// Table 2 defines the ML-DSA-65 private key as a 4032-byte array, which is /// used in the ML-DSA.Sign function in Algorithm 2, representing the /// expanded format. However, in -/// [draft-ietf-lamps-dilithium-certificates-08](https://datatracker.ietf.org/doc/html/draft-ietf-lamps-dilithium-certificates#name-private-key-format), +/// [RFC 9881](https://datatracker.ietf.org/doc/html/rfc9881#name-private-key-format), /// a private key can be encoded into a CHOICE of three formats, none in the /// same as the FIPS 204 format. The choices are defined in /// [sun.security.util.KeyChoices]. A `NamedKeyPairGenerator` implementation diff --git a/src/java.base/share/classes/sun/security/util/KeyChoices.java b/src/java.base/share/classes/sun/security/util/KeyChoices.java index 68f260e443d..6239591ccfe 100644 --- a/src/java.base/share/classes/sun/security/util/KeyChoices.java +++ b/src/java.base/share/classes/sun/security/util/KeyChoices.java @@ -44,6 +44,8 @@ import java.util.function.BiFunction; * } * * This class supports reading, writing, and convert between them. + *
+ * Current code follows draft-ietf-lamps-kyber-certificates-11 and RFC 9881.
*/
public final class KeyChoices {
diff --git a/src/java.base/share/conf/security/java.security b/src/java.base/share/conf/security/java.security
index 6ff26a97c6f..b98f4c511a4 100644
--- a/src/java.base/share/conf/security/java.security
+++ b/src/java.base/share/conf/security/java.security
@@ -1653,25 +1653,26 @@ jdk.tls.alpnCharset=ISO_8859_1
jdk.epkcs8.defaultAlgorithm=PBEWithHmacSHA256AndAES_128
#
-# Newly created ML-KEM and ML-DSA private key formats in PKCS #8
+# PKCS #8 encoding format for newly created ML-KEM and ML-DSA private keys
#
-# The draft-ietf-lamps-kyber-certificates and draft-ietf-lamps-dilithium-certificates
-# specifications define three formats for a private key: a seed (64 bytes for ML-KEM,
-# 32 bytes for ML-DSA), an expanded private key, or a sequence containing both.
+# draft-ietf-lamps-kyber-certificates-11 and RFC 9881 define three possible formats for a private key:
+# a seed (64 bytes for ML-KEM, 32 bytes for ML-DSA), an expanded private key,
+# or a sequence containing both.
+#
+# The following security properties determine the encoding format used when a
+# new keypair is generated with a KeyPairGenerator, and the output of the
+# translateKey method on an existing key using a ML-KEM or ML-DSA KeyFactory.
#
# Valid values for these properties are "seed", "expandedKey", and "both"
# (case-insensitive). The default is "seed".
#
-# These properties determine the encoding format used when a new keypair is generated
-# using a KeyPairGenerator, as well as the output of the translateKey method on an
-# existing key using a ML-KEM or ML-DSA KeyFactory.
-#
# If a system property of the same name is also specified, it supersedes the
# security property value defined here.
#
# Note: These properties are currently used by the SunJCE (for ML-KEM) and SUN
-# (for ML-DSA) providers in the JDK Reference implementation. They are not guaranteed
-# to be supported by other SE implementations or third-party security providers.
+# (for ML-DSA) providers in the JDK Reference implementation. They are not
+# guaranteed to be supported by other implementations or third-party security
+# providers.
#
#jdk.mlkem.pkcs8.encoding = seed
#jdk.mldsa.pkcs8.encoding = seed
diff --git a/test/jdk/sun/security/provider/pqc/PrivateKeyEncodings.java b/test/jdk/sun/security/provider/pqc/PrivateKeyEncodings.java
new file mode 100644
index 00000000000..baf3aeb0116
--- /dev/null
+++ b/test/jdk/sun/security/provider/pqc/PrivateKeyEncodings.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 8347938 8347941
+ * @library /test/lib
+ * @summary ensure ML-KEM and ML-DSA encodings consistent with
+ * draft-ietf-lamps-kyber-certificates-11 and RFC 9881
+ * @modules java.base/com.sun.crypto.provider
+ * java.base/sun.security.pkcs
+ * java.base/sun.security.provider
+ * @run main/othervm PrivateKeyEncodings
+ */
+import com.sun.crypto.provider.ML_KEM_Impls;
+import jdk.test.lib.Asserts;
+import jdk.test.lib.security.RepositoryFileReader;
+import jdk.test.lib.security.FixedSecureRandom;
+import sun.security.pkcs.NamedPKCS8Key;
+import sun.security.provider.ML_DSA_Impls;
+
+import javax.crypto.KEM;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.cert.CertificateFactory;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.NamedParameterSpec;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class PrivateKeyEncodings {
+
+ public static void main(String[] args) throws Exception {
+ // Example keys and certificates draft-ietf-lamps-kyber-certificates-11, Appendix B
+ // (https://datatracker.ietf.org/doc/html/draft-ietf-lamps-kyber-certificates-11#autoid-17)
+ // and RFC 9881, Appendix C.3
+ // (https://datatracker.ietf.org/doc/html/rfc9881#name-example-certificates)
+ //
+ // These data can be retrieved from the following GitHub releases:
+ // https://github.com/lamps-wg/kyber-certificates/releases/tag/draft-ietf-lamps-kyber-certificates-11
+ // https://github.com/lamps-wg/dilithium-certificates/releases/tag/draft-ietf-lamps-dilithium-certificates-13
+ //
+ // Although the release tags include "draft", these values are the
+ // same as those in the final RFC 9881.
+ try (var kemReader = RepositoryFileReader.of(RepositoryFileReader.KYBER_CERTIFICATES.class,
+ "kyber-certificates-draft-ietf-lamps-kyber-certificates-11/");
+ var dsaReader = RepositoryFileReader.of(RepositoryFileReader.DILITHIUM_CERTIFICATES.class,
+ "dilithium-certificates-draft-ietf-lamps-dilithium-certificates-13/")) {
+ good(kemReader, dsaReader);
+ badkem(kemReader);
+ baddsa(dsaReader);
+ }
+ }
+
+ static void badkem(RepositoryFileReader f) throws Exception {
+ var kf = KeyFactory.getInstance("ML-KEM");
+
+ // The first ML-KEM-512-PrivateKey example includes the both CHOICE,
+ // i.e., both seed and expandedKey are included. The seed and expanded
+ // values can be checked for inconsistencies.
+ Asserts.assertThrows(InvalidKeySpecException.class, () ->
+ kf.generatePrivate(new PKCS8EncodedKeySpec(
+ readData(f, "example/bad-ML-KEM-512-1.priv"))));
+
+ // The second ML-KEM-512-PrivateKey example includes only expandedKey.
+ // The expanded private key has a mutated s_0 and a valid public key hash,
+ // but a pairwise consistency check would find that the public key
+ // fails to match private.
+ var k2 = kf.generatePrivate(new PKCS8EncodedKeySpec(
+ readData(f, "example/bad-ML-KEM-512-2.priv")));
+ var pk2 = ML_KEM_Impls.privKeyToPubKey((NamedPKCS8Key) k2);
+ var enc = KEM.getInstance("ML-KEM").newEncapsulator(pk2).encapsulate();
+ var dk = KEM.getInstance("ML-KEM").newDecapsulator(k2).decapsulate(enc.encapsulation());
+ Asserts.assertNotEqualsByteArray(enc.key().getEncoded(), dk.getEncoded());
+
+ // The third ML-KEM-512-PrivateKey example includes only expandedKey.
+ // The expanded private key has a mutated H(ek); both a public key
+ // digest check and a pairwise consistency check should fail.
+ var k3 = kf.generatePrivate(new PKCS8EncodedKeySpec(
+ readData(f, "example/bad-ML-KEM-512-3.priv")));
+ Asserts.assertThrows(InvalidKeyException.class, () ->
+ KEM.getInstance("ML-KEM").newDecapsulator(k3));
+
+ // The fourth ML-KEM-512-PrivateKey example includes the both CHOICE,
+ // i.e., both seed and expandedKey are included. There is mismatch
+ // of the seed and expanded private key in only the z implicit rejection
+ // secret; here the private and public vectors match and the pairwise
+ // consistency check passes, but z is different.
+ Asserts.assertThrows(InvalidKeySpecException.class, () ->
+ kf.generatePrivate(new PKCS8EncodedKeySpec(
+ readData(f, "example/bad-ML-KEM-512-4.priv"))));
+ }
+
+ static void baddsa(RepositoryFileReader f) throws Exception {
+ var kf = KeyFactory.getInstance("ML-DSA");
+
+ // The first ML-DSA-PrivateKey example includes the both CHOICE, i.e.,
+ // both seed and expandedKey are included. The seed and expanded values
+ // can be checked for inconsistencies.
+ Asserts.assertThrows(InvalidKeySpecException.class, () ->
+ kf.generatePrivate(new PKCS8EncodedKeySpec(
+ readData(f, "examples/bad-ML-DSA-44-1.priv"))));
+
+ // The second ML-DSA-PrivateKey example includes only expandedKey.
+ // The public key fails to match the tr hash value in the private key.
+ var k2 = kf.generatePrivate(new PKCS8EncodedKeySpec(
+ readData(f, "examples/bad-ML-DSA-44-2.priv")));
+ Asserts.assertThrows(IllegalArgumentException.class, () ->
+ ML_DSA_Impls.privKeyToPubKey((NamedPKCS8Key) k2));
+
+ // The third ML-DSA-PrivateKey example also includes only expandedKey.
+ // The private s_1 and s_2 vectors imply a t vector whose private low
+ // bits do not match the t_0 vector portion of the private key
+ // (its high bits t_1 are the primary content of the public key).
+ var k3 = kf.generatePrivate(new PKCS8EncodedKeySpec(
+ readData(f, "examples/bad-ML-DSA-44-3.priv")));
+ Asserts.assertThrows(IllegalArgumentException.class, () ->
+ ML_DSA_Impls.privKeyToPubKey((NamedPKCS8Key) k3));
+ }
+
+ static void good(RepositoryFileReader kemReader, RepositoryFileReader dsaReader)
+ throws Exception {
+
+ var seed = new byte[64];
+ for (var i = 0; i < seed.length; i++) {
+ seed[i] = (byte) i;
+ }
+ var cf = CertificateFactory.getInstance("X.509");
+ var allPublicKeys = new HashMap