diff --git a/src/java.base/share/classes/sun/security/pkcs/PKCS7.java b/src/java.base/share/classes/sun/security/pkcs/PKCS7.java index 324cc3e7cec..a6cd5eba474 100644 --- a/src/java.base/share/classes/sun/security/pkcs/PKCS7.java +++ b/src/java.base/share/classes/sun/security/pkcs/PKCS7.java @@ -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 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 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(); diff --git a/src/java.base/share/classes/sun/security/pkcs/SignerInfo.java b/src/java.base/share/classes/sun/security/pkcs/SignerInfo.java index b1b8fea8353..36139d4f4a0 100644 --- a/src/java.base/share/classes/sun/security/pkcs/SignerInfo.java +++ b/src/java.base/share/classes/sun/security/pkcs/SignerInfo.java @@ -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() { diff --git a/src/java.base/share/classes/sun/security/util/KeyUtil.java b/src/java.base/share/classes/sun/security/util/KeyUtil.java index dd27b5f02d8..ccc72ea6ea2 100644 --- a/src/java.base/share/classes/sun/security/util/KeyUtil.java +++ b/src/java.base/share/classes/sun/security/util/KeyUtil.java @@ -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) { diff --git a/src/java.base/share/classes/sun/security/util/SignatureUtil.java b/src/java.base/share/classes/sun/security/util/SignatureUtil.java index 05cad48c042..000051a702c 100644 --- a/src/java.base/share/classes/sun/security/util/SignatureUtil.java +++ b/src/java.base/share/classes/sun/security/util/SignatureUtil.java @@ -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)); } diff --git a/src/java.base/share/classes/sun/security/x509/AlgorithmId.java b/src/java.base/share/classes/sun/security/x509/AlgorithmId.java index 8d2c761a011..7cbb5484b37 100644 --- a/src/java.base/share/classes/sun/security/x509/AlgorithmId.java +++ b/src/java.base/share/classes/sun/security/x509/AlgorithmId.java @@ -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); diff --git a/src/jdk.jartool/share/man/jarsigner.md b/src/jdk.jartool/share/man/jarsigner.md index 2c921775505..0ecc159a037 100644 --- a/src/jdk.jartool/share/man/jarsigner.md +++ b/src/jdk.jartool/share/man/jarsigner.md @@ -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. diff --git a/test/jdk/sun/security/pkcs/pkcs7/MLDSADigestConformance.java b/test/jdk/sun/security/pkcs/pkcs7/MLDSADigestConformance.java new file mode 100644 index 00000000000..80aa0c96281 --- /dev/null +++ b/test/jdk/sun/security/pkcs/pkcs7/MLDSADigestConformance.java @@ -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> 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; + } + } +} diff --git a/test/jdk/sun/security/provider/acvp/Launcher.java b/test/jdk/sun/security/provider/acvp/Launcher.java index 2f25b370c11..680d1026275 100644 --- a/test/jdk/sun/security/provider/acvp/Launcher.java +++ b/test/jdk/sun/security/provider/acvp/Launcher.java @@ -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 diff --git a/test/jdk/sun/security/provider/pqc/ML_DSA_CMS.java b/test/jdk/sun/security/provider/pqc/ML_DSA_CMS.java new file mode 100644 index 00000000000..f7469847b3f --- /dev/null +++ b/test/jdk/sun/security/provider/pqc/ML_DSA_CMS.java @@ -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)); + } +} diff --git a/test/jdk/sun/security/tools/jarsigner/ML_DSA.java b/test/jdk/sun/security/tools/jarsigner/ML_DSA.java new file mode 100644 index 00000000000..55758b761b3 --- /dev/null +++ b/test/jdk/sun/security/tools/jarsigner/ML_DSA.java @@ -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 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)); + } +} diff --git a/test/lib/jdk/test/lib/security/RepositoryFileReader.java b/test/lib/jdk/test/lib/security/RepositoryFileReader.java new file mode 100644 index 00000000000..4eefc7d82db --- /dev/null +++ b/test/lib/jdk/test/lib/security/RepositoryFileReader.java @@ -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 { + } +}