8349732: Add support for JARs signed with ML-DSA

Reviewed-by: mullan
This commit is contained in:
Weijun Wang 2025-11-10 14:39:22 +00:00
parent 1877ff996b
commit 2d4f2fde22
11 changed files with 717 additions and 103 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1996, 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
@ -530,8 +530,23 @@ public class PKCS7 {
* @exception SignatureException on signature handling errors.
*/
public SignerInfo verify(SignerInfo info, byte[] bytes)
throws NoSuchAlgorithmException, SignatureException {
return info.verify(this, bytes);
throws NoSuchAlgorithmException, SignatureException {
return info.verify(this, bytes, null);
}
/**
* This verifies a given SignerInfo.
*
* @param info the signer information.
* @param bytes the DER encoded content information.
* @param cert certificate used to verify; find one inside the block if null
*
* @exception NoSuchAlgorithmException on unrecognized algorithms.
* @exception SignatureException on signature handling errors.
*/
public SignerInfo verify(SignerInfo info, byte[] bytes, X509Certificate cert)
throws NoSuchAlgorithmException, SignatureException {
return info.verify(this, bytes, cert);
}
/**
@ -715,6 +730,19 @@ public class PKCS7 {
return this.oldStyle;
}
// Generate signed data without a specified digAlgID.
public static byte[] generateSignedData(
String sigalg, Provider sigProvider,
PrivateKey privateKey, X509Certificate[] signerChain,
byte[] content, boolean internalsf, boolean directsign,
Function<byte[], PKCS9Attributes> ts)
throws SignatureException, InvalidKeyException, IOException,
NoSuchAlgorithmException {
return generateSignedData(sigalg, sigProvider, privateKey, signerChain,
content, internalsf, directsign,
null, ts);
}
/**
* Generate a PKCS7 data block.
*
@ -725,6 +753,7 @@ public class PKCS7 {
* @param content the content to sign
* @param internalsf whether the content should be included in output
* @param directsign if the content is signed directly or through authattrs
* @param digAlgID digest alg to use; derive from other arguments if null
* @param ts (optional) timestamper
* @return the pkcs7 output in an array
* @throws SignatureException if signing failed
@ -736,14 +765,17 @@ public class PKCS7 {
String sigalg, Provider sigProvider,
PrivateKey privateKey, X509Certificate[] signerChain,
byte[] content, boolean internalsf, boolean directsign,
AlgorithmId digAlgID,
Function<byte[], PKCS9Attributes> ts)
throws SignatureException, InvalidKeyException, IOException,
NoSuchAlgorithmException {
Signature signer = SignatureUtil.fromKey(sigalg, privateKey, sigProvider);
AlgorithmId digAlgID = SignatureUtil.getDigestAlgInPkcs7SignerInfo(
signer, sigalg, privateKey, signerChain[0].getPublicKey(), directsign);
if (digAlgID == null) {
digAlgID = SignatureUtil.getDigestAlgInPkcs7SignerInfo(
signer, sigalg, privateKey, signerChain[0].getPublicKey(), directsign);
}
AlgorithmId sigAlgID = SignatureUtil.fromSignature(signer, privateKey);
PKCS9Attributes authAttrs = null;
@ -751,8 +783,9 @@ public class PKCS7 {
// MessageDigest
byte[] md;
String digAlgName = digAlgID.getName();
if (digAlgName.equals("SHAKE256") || digAlgName.equals("SHAKE256-LEN")) {
// No MessageDigest impl for SHAKE256 yet
if (digAlgName.equals("SHAKE256-LEN")) {
// We don't check the LEN here. Usually it is returned
// by SignatureUtil.getDigestAlgInPkcs7SignerInfo
var shaker = new SHAKE256(64);
shaker.update(content, 0, content.length);
md = shaker.digest();

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1996, 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
@ -303,15 +303,20 @@ public class SignerInfo implements DerEncoder {
return certList;
}
/* Returns null if verify fails, this signerInfo if
verify succeeds. */
SignerInfo verify(PKCS7 block, byte[] data)
throws NoSuchAlgorithmException, SignatureException {
/**
* Verify this signerInfo in a PKCS7 block.
*
* @param block the PKCS7 object
* @param data the content to verify against; read from block if null
* @param cert certificate to verify with; read from block if null
* @return null if verify fails, this signerInfo if verify succeeds.
*/
SignerInfo verify(PKCS7 block, byte[] data, X509Certificate cert)
throws NoSuchAlgorithmException, SignatureException {
try {
Timestamp timestamp = null;
try {
timestamp = getTimestamp();
getTimestamp();
} catch (Exception e) {
// Log exception and continue. This allows for the case
// where, if there are no other errors, the code is
@ -356,22 +361,19 @@ public class SignerInfo implements DerEncoder {
return null;
byte[] computedMessageDigest;
if (digestAlgName.equals("SHAKE256")
|| digestAlgName.equals("SHAKE256-LEN")) {
if (digestAlgName.equals("SHAKE256-LEN")) {
// RFC8419: for EdDSA in CMS, the id-shake256-len
// algorithm id must contain parameter value 512
// encoded as a positive integer value
byte[] params = digestAlgorithmId.getEncodedParams();
if (params == null) {
throw new SignatureException(
"id-shake256-len oid missing length");
}
int v = new DerValue(params).getInteger();
if (v != 512) {
throw new SignatureException(
"Unsupported id-shake256-" + v);
}
if (digestAlgName.equals("SHAKE256-LEN")) {
// RFC8419: for EdDSA in CMS, the id-shake256-len
// algorithm id must contain parameter value 512
// encoded as a positive integer value
byte[] params = digestAlgorithmId.getEncodedParams();
if (params == null) {
throw new SignatureException(
"id-shake256-len oid missing length");
}
int v = new DerValue(params).getInteger();
if (v != 512) {
throw new SignatureException(
"Unsupported id-shake256-" + v);
}
var md = new SHAKE256(64);
md.update(data, 0, data.length);
@ -410,9 +412,11 @@ public class SignerInfo implements DerEncoder {
"SignerInfo digestEncryptionAlgorithm field", true));
}
X509Certificate cert = getCertificate(block);
if (cert == null) {
return null;
cert = getCertificate(block);
if (cert == null) {
return null;
}
}
PublicKey key = cert.getPublicKey();
@ -503,29 +507,59 @@ public class SignerInfo implements DerEncoder {
}
if (!AlgorithmId.get(spec.getDigestAlgorithm()).equals(digAlgId)) {
throw new NoSuchAlgorithmException("Incompatible digest algorithm");
throw new NoSuchAlgorithmException("Incompatible digest algorithm " + digAlgId);
}
break;
case "Ed25519":
if (!digAlgId.equals(SignatureUtil.EdDSADigestAlgHolder.sha512)) {
throw new NoSuchAlgorithmException("Incompatible digest algorithm");
if (!digAlgId.equalsOID(AlgorithmId.SHA512_oid)) {
throw new NoSuchAlgorithmException("Incompatible digest algorithm " + digAlgId);
}
break;
case "Ed448":
if (directSign) {
if (!digAlgId.equals(SignatureUtil.EdDSADigestAlgHolder.shake256)) {
throw new NoSuchAlgorithmException("Incompatible digest algorithm");
if (!digAlgId.equalsOID(AlgorithmId.SHAKE256_512_oid)) {
throw new NoSuchAlgorithmException("Incompatible digest algorithm " + digAlgId);
}
} else {
if (!digAlgId.equals(SignatureUtil.EdDSADigestAlgHolder.shake256$512)) {
throw new NoSuchAlgorithmException("Incompatible digest algorithm");
if (!digAlgId.equals(SignatureUtil.DigestAlgHolder.shake256lenWith512)) {
throw new NoSuchAlgorithmException("Incompatible digest algorithm " + digAlgId);
}
}
break;
case "HSS/LMS":
// RFC 8708 requires the same hash algorithm used as in the HSS/LMS algorithm
if (!digAlgId.equals(AlgorithmId.get(KeyUtil.hashAlgFromHSS(key)))) {
throw new NoSuchAlgorithmException("Incompatible digest algorithm");
if (!digAlgId.equalsOID(KeyUtil.hashAlgFromHSS(key))) {
throw new NoSuchAlgorithmException("Incompatible digest algorithm " + digAlgId);
}
break;
case "ML-DSA-44":
// Following 3 from Table 1 inside
// https://datatracker.ietf.org/doc/html/rfc9882#name-signerinfo-content
if (!digAlgId.equalsOID(AlgorithmId.SHA256_oid)
&& !digAlgId.equalsOID(AlgorithmId.SHA384_oid)
&& !digAlgId.equalsOID(AlgorithmId.SHA512_oid)
&& !digAlgId.equalsOID(AlgorithmId.SHA3_256_oid)
&& !digAlgId.equalsOID(AlgorithmId.SHA3_384_oid)
&& !digAlgId.equalsOID(AlgorithmId.SHA3_512_oid)
&& !digAlgId.equalsOID(AlgorithmId.SHAKE128_256_oid)
&& !digAlgId.equalsOID(AlgorithmId.SHAKE256_512_oid)) {
throw new NoSuchAlgorithmException("Incompatible digest algorithm " + digAlgId);
}
break;
case "ML-DSA-65":
if (!digAlgId.equalsOID(AlgorithmId.SHA384_oid)
&& !digAlgId.equalsOID(AlgorithmId.SHA512_oid)
&& !digAlgId.equalsOID(AlgorithmId.SHA3_384_oid)
&& !digAlgId.equalsOID(AlgorithmId.SHA3_512_oid)
&& !digAlgId.equalsOID(AlgorithmId.SHAKE256_512_oid)) {
throw new NoSuchAlgorithmException("Incompatible digest algorithm " + digAlgId);
}
break;
case "ML-DSA-87":
if (!digAlgId.equalsOID(AlgorithmId.SHA512_oid)
&& !digAlgId.equalsOID(AlgorithmId.SHA3_512_oid)
&& !digAlgId.equalsOID(AlgorithmId.SHAKE256_512_oid)) {
throw new NoSuchAlgorithmException("Incompatible digest algorithm " + digAlgId);
}
break;
}
@ -538,9 +572,9 @@ public class SignerInfo implements DerEncoder {
* The digest algorithm is in the form "DIG", and the encryption
* algorithm can be in any of the 3 forms:
*
* 1. Old style key algorithm like RSA, DSA, EC, this method returns
* 1. Simple key algorithm like RSA, DSA, EC, this method returns
* DIGwithKEY.
* 2. New style signature algorithm in the form of HASHwithKEY, this
* 2. Traditional signature algorithm in the form of HASHwithKEY, this
* method returns DIGwithKEY. Please note this is not HASHwithKEY.
* 3. Modern signature algorithm like RSASSA-PSS and EdDSA, this method
* returns the signature algorithm itself.
@ -550,40 +584,26 @@ public class SignerInfo implements DerEncoder {
*/
public static String makeSigAlg(AlgorithmId digAlgId, AlgorithmId encAlgId) {
String encAlg = encAlgId.getName();
switch (encAlg) {
case "RSASSA-PSS":
case "Ed25519":
case "Ed448":
case "HSS/LMS":
return encAlg;
default:
String digAlg = digAlgId.getName();
String keyAlg = SignatureUtil.extractKeyAlgFromDwithE(encAlg);
if (keyAlg == null) {
// The encAlg used to be only the key alg
keyAlg = encAlg;
}
if (digAlg.startsWith("SHA-")) {
digAlg = "SHA" + digAlg.substring(4);
}
if (keyAlg.equals("EC")) keyAlg = "ECDSA";
String sigAlg = digAlg + "with" + keyAlg;
try {
Signature.getInstance(sigAlg);
return sigAlg;
} catch (NoSuchAlgorithmException e) {
// Possibly an unknown modern signature algorithm,
// in this case, encAlg should already be a signature
// algorithm.
return encAlg;
}
String keyAlg = SignatureUtil.extractKeyAlgFromDwithE(encAlg);
if (keyAlg == null) { // No "WITH" inside
if (encAlg.equals("RSA") || encAlg.equals("DSA") || encAlg.equals("EC")) {
keyAlg = encAlg; // Sometimes encAlgId is just the enc alg
} else {
return encAlg; // Must be a modern algorithm like EdDSA or ML-DSA
}
}
String digAlg = digAlgId.getName();
if (digAlg.startsWith("SHA-")) {
digAlg = "SHA" + digAlg.substring(4);
}
if (keyAlg.equals("EC")) keyAlg = "ECDSA";
return digAlg + "with" + keyAlg;
}
/* Verify the content of the pkcs7 block. */
SignerInfo verify(PKCS7 block)
throws NoSuchAlgorithmException, SignatureException {
return verify(block, null);
return verify(block, null, null);
}
public BigInteger getVersion() {

View File

@ -431,7 +431,7 @@ public final class KeyUtil {
* @return the hash algorithm
* @throws NoSuchAlgorithmException if key is from an unknown configuration
*/
public static String hashAlgFromHSS(PublicKey publicKey)
public static ObjectIdentifier hashAlgFromHSS(PublicKey publicKey)
throws NoSuchAlgorithmException {
try {
DerValue val = new DerValue(publicKey.getEncoded());
@ -450,7 +450,7 @@ public final class KeyUtil {
+ ((rawKey[6] & 0xff) << 8) + (rawKey[7] & 0xff);
return switch (num) {
// RFC 8554 only supports SHA_256 hash algorithm
case 5, 6, 7, 8, 9 -> "SHA-256";
case 5, 6, 7, 8, 9 -> AlgorithmId.SHA256_oid;
default -> throw new NoSuchAlgorithmException("Unknown LMS type: " + num);
};
} catch (IOException e) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 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
@ -190,16 +190,16 @@ public class SignatureUtil {
SharedSecrets.getJavaSecuritySignatureAccess().initSign(s, key, params, sr);
}
public static class EdDSADigestAlgHolder {
public static class DigestAlgHolder {
public static final AlgorithmId sha512;
public static final AlgorithmId shake256;
public static final AlgorithmId shake256$512;
public static final AlgorithmId shake256_512;
public static final AlgorithmId shake256lenWith512;
static {
try {
sha512 = new AlgorithmId(ObjectIdentifier.of(KnownOIDs.SHA_512));
shake256 = new AlgorithmId(ObjectIdentifier.of(KnownOIDs.SHAKE256_512));
shake256$512 = new AlgorithmId(
sha512 = new AlgorithmId(AlgorithmId.SHA512_oid);
shake256_512 = new AlgorithmId(AlgorithmId.SHAKE256_512_oid);
shake256lenWith512 = new AlgorithmId(
ObjectIdentifier.of(KnownOIDs.SHAKE256_LEN),
new DerValue((byte) 2, new byte[]{2, 0})); // int 512
} catch (IOException e) {
@ -233,18 +233,22 @@ public class SignatureUtil {
// https://www.rfc-editor.org/rfc/rfc8419.html#section-3
switch (kAlg.toUpperCase(Locale.ENGLISH)) {
case "ED25519":
digAlgID = EdDSADigestAlgHolder.sha512;
digAlgID = DigestAlgHolder.sha512;
break;
case "ED448":
if (directsign) {
digAlgID = EdDSADigestAlgHolder.shake256;
digAlgID = DigestAlgHolder.shake256_512;
} else {
digAlgID = EdDSADigestAlgHolder.shake256$512;
digAlgID = DigestAlgHolder.shake256lenWith512;
}
break;
default:
throw new AssertionError("Unknown curve name: " + kAlg);
}
} else if (kAlg.toUpperCase(Locale.ENGLISH).startsWith("ML-DSA")) {
// https://datatracker.ietf.org/doc/html/rfc9882#name-signerinfo-content
// Just use SHA-512
digAlgID = DigestAlgHolder.sha512;
} else if (sigalg.equalsIgnoreCase("RSASSA-PSS")) {
try {
digAlgID = AlgorithmId.get(signer.getParameters()
@ -254,7 +258,7 @@ public class SignatureUtil {
throw new AssertionError("Should not happen", e);
}
} else if (sigalg.equalsIgnoreCase("HSS/LMS")) {
digAlgID = AlgorithmId.get(KeyUtil.hashAlgFromHSS(publicKey));
digAlgID = new AlgorithmId(KeyUtil.hashAlgFromHSS(publicKey));
} else {
digAlgID = AlgorithmId.get(extractDigestAlgFromDwithE(sigalg));
}

View File

@ -307,6 +307,14 @@ public class AlgorithmId implements Serializable, DerEncoder {
Arrays.equals(encodedParams, other.encodedParams);
}
/**
* Returns true if this AlgorithmID only has the specified ObjectIdentifier
* and a NULL params. Note the encoded params might be ASN.1 NULL or absent.
*/
public boolean equalsOID(ObjectIdentifier oid) {
return algid.equals(oid) && encodedParams == null;
}
/**
* Compares this AlgorithmID to another. If algorithm parameters are
* available, they are compared. Otherwise, just the object IDs
@ -628,6 +636,12 @@ public class AlgorithmId implements Serializable, DerEncoder {
public static final ObjectIdentifier SHA3_512_oid =
ObjectIdentifier.of(KnownOIDs.SHA3_512);
public static final ObjectIdentifier SHAKE128_256_oid =
ObjectIdentifier.of(KnownOIDs.SHAKE128_256);
public static final ObjectIdentifier SHAKE256_512_oid =
ObjectIdentifier.of(KnownOIDs.SHAKE256_512);
public static final ObjectIdentifier DSA_oid =
ObjectIdentifier.of(KnownOIDs.DSA);

View File

@ -255,29 +255,35 @@ the private key:
Table: Default Signature Algorithms and Block File Extensions
keyalg key size default sigalg block file extension
------- -------- -------------- --------------------
DSA any size SHA256withDSA .DSA
RSA \< 624 SHA256withRSA .RSA
\<= 7680 SHA384withRSA
\> 7680 SHA512withRSA
EC \< 512 SHA384withECDSA .EC
\>= 512 SHA512withECDSA
RSASSA-PSS \< 624 RSASSA-PSS (with SHA-256) .RSA
\<= 7680 RSASSA-PSS (with SHA-384)
\> 7680 RSASSA-PSS (with SHA-512)
EdDSA 255 Ed25519 .EC
448 Ed448
------- -------- -------------- ------
keyalg key size default sigalg block file extension
------- -------- -------------- --------------------
DSA any size SHA256withDSA .DSA
RSA \< 624 SHA256withRSA .RSA
\<= 7680 SHA384withRSA
\> 7680 SHA512withRSA
EC \< 512 SHA384withECDSA .EC
\>= 512 SHA512withECDSA
RSASSA-PSS^1^ \< 624 RSASSA-PSS (with SHA-256) .RSA
\<= 7680 RSASSA-PSS (with SHA-384)
\> 7680 RSASSA-PSS (with SHA-512)
EdDSA^2^ EdDSA .EC
ML-DSA^2^ ML-DSA .DSA
------- -------- -------------- ------
* If an RSASSA-PSS key is encoded with parameters, then jarsigner will use the
1. If an RSASSA-PSS key is encoded with parameters, then jarsigner will use the
same parameters in the signature. Otherwise, jarsigner will use parameters that
are determined by the size of the key as specified in the table above.
For example, an 3072-bit RSASSA-PSS key will use RSASSA-PSS as the signature
algorithm and SHA-384 as the hash and MGF1 algorithms.
* If a key algorithm is not listed in this table, the `.DSA` extension
is used when signing a JAR file.
2. Modern digital signature algorithms such as EdDSA and ML-DSA use the same
name for both the key and signature algorithms. Only the signature algorithm
with the same name can be used with a given key algorithm. The specific
signature parameter set (for example, Ed25519 or Ed448 for EdDSA) is the
same as that of the key.
If a key algorithm is not listed in this table, the `.DSA` block file extension
is always used.
These default signature algorithms can be overridden by using the `-sigalg`
option.

View File

@ -0,0 +1,105 @@
/*
* 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 8349732
* @summary ML-DSA digest alg conformance check
* @modules java.base/sun.security.pkcs
* java.base/sun.security.tools.keytool
* java.base/sun.security.x509
* @library /test/lib
*/
import jdk.test.lib.Asserts;
import sun.security.pkcs.PKCS7;
import sun.security.tools.keytool.CertAndKeyGen;
import sun.security.x509.AlgorithmId;
import sun.security.x509.X500Name;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
public class MLDSADigestConformance {
static String[] ALL_KEY_ALGS = {"ML-DSA-44", "ML-DSA-65", "ML-DSA-87"};
static String[] ALL_DIGEST_ALGS = {
"SHA-1", "SHA-224", "SHA-256", "SHA-384", "SHA-512",
"SHA3-224", "SHA3-256", "SHA3-384", "SHA3-512",
"SHAKE128-256", "SHAKE256-512"};
static Map<String, List<String>> SUPPORTED = Map.of(
"ML-DSA-44", List.of("SHA-256", "SHA-384", "SHA-512",
"SHA3-256", "SHA3-384", "SHA3-512",
"SHAKE128-256", "SHAKE256-512"),
"ML-DSA-65", List.of("SHA-384", "SHA-512",
"SHA3-384", "SHA3-512", "SHAKE256-512"),
"ML-DSA-87", List.of("SHA-512", "SHA3-512", "SHAKE256-512")
);
public static void main(String[] args) throws Exception {
testSig("ML-DSA-44");
testSig("ML-DSA-65");
testSig("ML-DSA-87");
}
static void testSig(String keyAlg) throws Exception {
System.out.println("Testing " + keyAlg);
var cag = new CertAndKeyGen(keyAlg, keyAlg);
cag.generate(keyAlg);
var sk = cag.getPrivateKey();
var certs = new X509Certificate[] {
cag.getSelfCertificate(new X500Name("CN=Me"), 1000)
};
var count = testDigest(keyAlg, sk, certs, null);
System.out.println(" digestAlg default: " + count);
Asserts.assertEQ(count, 1);
for (var da : ALL_DIGEST_ALGS) {
count = testDigest(keyAlg, sk, certs, da);
System.out.println(" digestAlg " + da + ": " + count);
if (SUPPORTED.get(keyAlg).contains(da)) {
Asserts.assertEQ(count, 1);
} else {
Asserts.assertEQ(count, 0);
}
}
}
static int testDigest(String keyAlg, PrivateKey sk,
X509Certificate[] certs, String digestAlg) throws Exception {
var content = "hello".getBytes(StandardCharsets.UTF_8);
var p7 = PKCS7.generateSignedData(keyAlg, null,
sk, certs,
content, true, false,
digestAlg == null ? null : AlgorithmId.get(digestAlg),
null);
try {
return new PKCS7(p7).verify(null).length;
} catch (NoSuchAlgorithmException e) {
return 0;
}
}
}

View File

@ -37,15 +37,17 @@ import java.util.zip.ZipFile;
/*
* @test
* @bug 8342442 8345057
* @summary Test default implementation. Use othervm because
* ML_DSA_Impls.version might be modified
* @library /test/lib
* @modules java.base/sun.security.provider
* @run main/timeout=480 Launcher
* @run main/othervm/timeout=480 Launcher
*/
/*
* @test
* @summary Test verifying the intrinsic implementation.
* @bug 8342442 8345057
* @summary Test verifying the intrinsic implementation.
* @library /test/lib
* @modules java.base/sun.security.provider
* @run main/othervm/timeout=480 -Xcomp Launcher

View File

@ -0,0 +1,98 @@
/*
* 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 8349732
* @library /test/lib
* @summary Add support for JARs signed with ML-DSA
* @modules java.base/sun.security.pkcs
*/
import jdk.test.lib.Asserts;
import jdk.test.lib.security.RepositoryFileReader;
import sun.security.pkcs.PKCS7;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.stream.Collectors;
import static jdk.test.lib.security.RepositoryFileReader.*;
public class ML_DSA_CMS {
public static void main(String[] args) throws Exception {
// Example signed-data encodings from RFC 9882, Appendix B
// (https://datatracker.ietf.org/doc/html/rfc9882#name-examples), which
// can be verified by example certificates from 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/cms-ml-dsa/releases/tag/draft-ietf-lamps-cms-ml-dsa-07
// 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 RFCs 9881 and 9882.
try (var cmsReader = RepositoryFileReader.of(CMS_ML_DSA.class,
"cms-ml-dsa-draft-ietf-lamps-cms-ml-dsa-07/");
var dsaReader = RepositoryFileReader.of(DILITHIUM_CERTIFICATES.class,
"dilithium-certificates-draft-ietf-lamps-dilithium-certificates-13/")) {
test(readCMS(cmsReader, "mldsa44-signed-attrs.pem"),
readCert(dsaReader, "ML-DSA-44.crt"));
test(readCMS(cmsReader, "mldsa65-signed-attrs.pem"),
readCert(dsaReader, "ML-DSA-65.crt"));
test(readCMS(cmsReader, "mldsa87-signed-attrs.pem"),
readCert(dsaReader, "ML-DSA-87.crt"));
}
}
/// Verifies a signed file.
/// @param data the signed data in PKCS #7 format
/// @param cert the certificate used to verify
static void test(byte[] data, X509Certificate cert) throws Exception {
var p7 = new PKCS7(data);
for (var si : p7.getSignerInfos()) {
Asserts.assertTrue(p7.verify(si, null, cert).getIssuerName() != null);
}
}
// Read data in https://datatracker.ietf.org/doc/html/rfc9882#name-examples
static byte[] readCMS(RepositoryFileReader f, String entry) throws IOException {
var data = f.read("examples/" + entry);
var pem = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(data)))
.lines()
.filter(s -> !s.contains("-----"))
.collect(Collectors.joining());
return Base64.getMimeDecoder().decode(pem);
}
// Read data in https://datatracker.ietf.org/doc/html/rfc9881#name-example-certificates
static X509Certificate readCert(RepositoryFileReader f, String entry) throws Exception {
var data = f.read("examples/" + entry);
var cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(data));
}
}

View File

@ -0,0 +1,181 @@
/*
* 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 8298387
* @summary signing with ML-DSA
* @library /test/lib
* @modules java.base/sun.security.util
*/
import jdk.security.jarsigner.JarSigner;
import jdk.test.lib.Asserts;
import jdk.test.lib.SecurityTools;
import jdk.test.lib.process.Proc;
import jdk.test.lib.security.DerUtils;
import jdk.test.lib.util.JarUtils;
import sun.security.util.KnownOIDs;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.util.List;
import java.util.jar.JarFile;
import java.util.zip.ZipFile;
public class ML_DSA {
static List<String> SIGNERS = List.of("44", "65", "87");
public static void main(String[] args) throws Exception {
if (args.length > 0) {
// Launched by testDisabled() in a sub-process to count signers
var expectedCount = Integer.parseInt(args[0]);
try (var jf = new JarFile("a.jar")) {
var je = jf.getJarEntry("a");
jf.getInputStream(je).readAllBytes();
var signers = je.getCodeSigners();
var count = signers == null ? 0 : signers.length;
if (expectedCount != count) {
throw new RuntimeException("Expected: " + expectedCount
+ ", actual " + count);
}
}
} else {
prepare();
testAPI();
testTool(); // call this last, as it modifies a.jar.
testDisabled();
}
}
private static void prepare() throws Exception {
for (var signer : SIGNERS) {
SecurityTools.keytool("-keystore ks -storepass changeit -genkeypair -alias "
+ signer + " -keyalg ML-DSA-" + signer + " -dname CN=" + signer)
.shouldHaveExitValue(0);
}
JarUtils.createJarFile(Path.of("a.jar"), Path.of("."),
Files.write(Path.of("a"), new byte[10]));
}
static void testAPI() throws Exception {
var pass = "changeit".toCharArray();
var ks = KeyStore.getInstance(new File("ks"), pass);
for (var signer : SIGNERS) {
var jsb = new JarSigner.Builder((KeyStore.PrivateKeyEntry)
ks.getEntry(signer, new KeyStore.PasswordProtection(pass)));
try (var zf = new ZipFile("a.jar");
var of = Files.newOutputStream(Path.of(signer + ".jar"))) {
jsb.signerName(signer).build().sign(zf, of);
}
try (var jf = new JarFile(signer + ".jar")) {
var je = jf.getJarEntry("a");
jf.getInputStream(je).readAllBytes();
Asserts.assertEquals(1, je.getCodeSigners().length);
checkDigestAlgorithm(jf, signer, KnownOIDs.SHA_512);
}
}
}
static void testTool() throws Exception {
for (var signer : SIGNERS) {
SecurityTools.jarsigner("-keystore ks -storepass changeit a.jar " + signer)
.shouldHaveExitValue(0);
}
SecurityTools.jarsigner("-verify a.jar -verbose -certs")
.shouldHaveExitValue(0)
.shouldContain("jar verified");
try (var jf = new JarFile("a.jar")) {
var je = jf.getJarEntry("a");
jf.getInputStream(je).readAllBytes();
Asserts.assertEquals(3, je.getCodeSigners().length);
for (var signer : SIGNERS) {
checkDigestAlgorithm(jf, signer, KnownOIDs.SHA_512);
}
}
}
static void testDisabled() throws Exception {
// All disabled
Files.writeString(Paths.get("my.security"),
"jdk.jar.disabledAlgorithms=ML-DSA");
SecurityTools.jarsigner("-J-Djava.security.properties=my.security" +
" -keystore ks -storepass changeit" +
" -verify a.jar -verbose -certs -strict")
.shouldHaveExitValue(16)
.shouldContain("ML-DSA-44 key (disabled)")
.shouldContain("ML-DSA-65 key (disabled)")
.shouldContain("ML-DSA-87 key (disabled)")
.shouldContain("The jar will be treated as unsigned");
// Need to launch in a new process because security property is
// read at the beginning
Proc.create("ML_DSA")
.secprop("jdk.jar.disabledAlgorithms", "ML-DSA")
.args("0")
.start()
.waitFor(0);
// One disabled, one made weak
Files.writeString(Paths.get("my.security"), """
jdk.jar.disabledAlgorithms=ML-DSA-44
jdk.security.legacyAlgorithms=ML-DSA-65
""");
SecurityTools.jarsigner("-J-Djava.security.properties=my.security" +
" -keystore ks -storepass changeit" +
" -verify a.jar -verbose -certs -strict")
.shouldHaveExitValue(0)
.shouldContain("ML-DSA-44 key (disabled)")
.shouldContain("ML-DSA-65 key (weak)")
.shouldContain("ML-DSA-87 key")
.shouldNotContain("ML-DSA-87 key (disabled)")
.shouldContain("jar verified.");
Proc.create("ML_DSA")
.secprop("jdk.jar.disabledAlgorithms", "ML-DSA-44")
.secprop("jdk.security.legacyAlgorithms", "ML-DSA-65")
.args("2") // weak still considered signer, disabled not
.start()
.waitFor(0);
}
static void checkDigestAlgorithm(JarFile jf, String alias, KnownOIDs digAlg)
throws Exception {
var p7 = jf.getInputStream(jf.getEntry("META-INF/" + alias + ".DSA"))
.readAllBytes();
// SignedData - digestAlgorithms
DerUtils.checkAlg(p7, "10100", digAlg);
// SignedData - signerInfos - digestAlgorithm
DerUtils.checkAlg(p7, "104020", digAlg);
// SignedData - signerInfos - signatureAlgorithm
DerUtils.checkAlg(p7, "104040", KnownOIDs.valueOf("ML_DSA_" + alias));
// SignedData - signerInfos - signedAttrs - CMSAlgorithmProtection - digestAlgorithm
DerUtils.checkAlg(p7, "1040321000", digAlg);
// SignedData - signerInfos - signedAttrs - CMSAlgorithmProtection - signatureAlgorithm
DerUtils.checkAlg(p7, "1040321010", KnownOIDs.valueOf("ML_DSA_" + alias));
}
}

View File

@ -0,0 +1,151 @@
/*
* 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.
*/
package jdk.test.lib.security;
import jdk.test.lib.artifacts.Artifact;
import jdk.test.lib.artifacts.ArtifactResolver;
import jdk.test.lib.artifacts.ArtifactResolverException;
import jtreg.SkippedException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/// A helper class to read files from a code repository.
///
/// By default, the code repository is stored on the artifact server
/// as a ZIP file.
///
/// Users can specify the "jdk.tests.repos.pattern" system property to read
/// the files from an alternative location. The value of this system property
/// represents the URL for each file entry where:
/// - "%o" maps to the last part of [organization name][Artifact#organization()],
/// - "%n" maps to [name][Artifact#name()],
/// - "%r" maps to [version][Artifact#revision()],
/// - "%e" maps to the name of the file entry to read.
///
/// For example, with the [CMS_ML_DSA] class inside this test:
/// - The pattern `file:///Users/tester/repos/external/%o/%n/%e` resolves to
/// a local file like `/Users/tester/repos/external/lamps-wg/cms-ml-dsa/entry`.
/// - The pattern `https://raw.repos.com/%o/%n/%r/%e` resolves to
/// `https://raw.repos.com/lamps-wg/cms-ml-dsa/c8f0cf7/entry`.
///
public sealed interface RepositoryFileReader extends AutoCloseable {
/// Reads the content of `entry` as a byte array.
byte[] read(String entry) throws IOException;
/// Overrides the method with a different exception type
/// to avoid compiler warnings about `InterruptedException`.
@Override
void close() throws IOException;
/// Returns a `RepositoryFileReader`.
/// @param klass the `Artifact` class
/// @param zipPrefix the prefix used in the ZIP file. See
/// [ZipReader#ZipReader(ZipFile, String)].
static RepositoryFileReader of(Class<?> klass, String zipPrefix) {
Artifact artifact = klass.getAnnotation(Artifact.class);
var org = artifact.organization();
var prop = System.getProperty("jdk.tests.repos.pattern");
if (prop != null && org.startsWith("jpg.tests.jdk.repos.")) {
prop = prop.replace("%o", org.substring(org.lastIndexOf('.') + 1));
prop = prop.replace("%n", artifact.name());
prop = prop.replace("%r", artifact.revision());
System.out.println("Creating URLReader on " + prop);
return new URLReader(prop);
} else {
try {
Path p = ArtifactResolver.resolve(klass).entrySet().stream()
.findAny().get().getValue();
System.out.println("Creating ZipReader on " + p);
return new ZipReader(new ZipFile(p.toFile()), zipPrefix);
} catch (ArtifactResolverException | IOException e) {
throw new SkippedException("Cannot find " + artifact.name(), e);
}
}
}
/// A `RepositoryFileReader` to read file from a URL.
/// @param base the base URL string, contains "%e" mapping to entry name
record URLReader(String base) implements RepositoryFileReader {
@Override
public void close() {
// nothing to do
}
@Override
public byte[] read(String entry) throws IOException {
System.out.println("Reading " + entry + "...");
try (var is = new URI(base.replace("%e", entry)).toURL().openStream()) {
return is.readAllBytes();
} catch (URISyntaxException e) {
throw new IOException("Cannot create URI", e);
}
}
}
/// A `RepositoryFileReader` to read file from a ZIP file.
/// @param zf the `ZipFile`
/// @param zipPrefix optional prefix string inside the ZIP file. For example,
/// if an entry "folder/file" is represented as "archive/folder/file"
/// inside the ZIP, "archive/" should be provided as `zipPrefix`.
record ZipReader(ZipFile zf, String zipPrefix) implements RepositoryFileReader {
@Override
public void close() throws IOException {
zf.close();
}
@Override
public byte[] read(String entry) throws IOException {
System.out.println("Reading " + entry + "...");
ZipEntry ze = zf.getEntry(zipPrefix + entry);
if (ze != null) {
return zf.getInputStream(ze).readAllBytes();
} else {
throw new RuntimeException("Entry not found: " + entry);
}
}
}
@Artifact(
organization = "jpg.tests.jdk.repos.lamps-wg",
name = "dilithium-certificates",
revision = "785a549",
extension = "zip",
unpack = false)
public static class DILITHIUM_CERTIFICATES {
}
@Artifact(
organization = "jpg.tests.jdk.repos.lamps-wg",
name = "cms-ml-dsa",
revision = "c8f0cf7",
extension = "zip",
unpack = false)
public static class CMS_ML_DSA {
}
}