/* * Copyright (c) 2017, 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 8177674 * @summary This test is used to verify the compatibility on jarsigner cross * different JDK releases. If property strict is true, it also checks whether * the default timestamp digest algorithm is SHA-256, and all of verification * must pass even if the jarsigner in a JDK build doesn't support a specific * algorithm. * * The test will generate a report, at JTwork/scratch/testReport, to display * the key parameters for signing and the status of signing and verifying. * * Please note that, the test may output a great deal of logs if the jdk list * is big, and that would lead to jtreg output overflow. So, it redirects * the stdout and stderr to file JTwork/scratch/test.out. * * The testing JDK, which is specified by jtreg option "-jdk", should include * the fix for JDK-8163304. Otherwise, the timestamp digest algorithm cannot * be extracted from verification output. * * Usage: jtreg [-options] \ * [-Dstrict= \ * -DproxyHost= -DproxyPort= \ * -Dtsa= \ * -DjdkListFile= \ * -DjdkList=] \ * /path/to/Compatibility.java * * Properties: * 1. strict= * If true, the test checks whether the default timestamp digest * algorithm is SHA-256, and all of verification must pass even if * the jarsigner in a JDK build doesn't support a specific algorithm. * The default value is true. * * 2. proxyHost= * This property indicates proxy host. * * 3. proxyPort= * This property indicates proxy port. The default value is 80. * * 4. tsa= * This property indicates a TSA service. It is mandatory. * * 5. jdkListFile= * This property indicates a local file, which contains a set of local * JDK paths. The style of the file content looks like the below, * /path/to/jdk1 * /path/to/jdk2 * /path/to/jdk3 * ... * * 6. jdkList= * This property directly lists a set of local JDK paths in command. * Note that, if both of jdkListFile and jdkList are specified, only * property jdkListFile is selected. If neither of jdkListFile and * jdkList is specified, the testing JDK, which is specified jtreg * option -jdk will be used as the only one JDK in the JDK list. * * Report columns: * 1. Signer JDK: The JDK version that signs jar. * 2. Signature Algorithm: The signature algorithm used by signing. * 3. TSA Digest Algorithm: The timestamp digest algorithm used by signing. * 4. Cert Type: Certificate types. * The types are the followings: * [1] Valid, normal self-signed certificate. * [2] Expired, expired self-signed certificate. * 5. Status of Signing: Signing process result status. * The status are the followings: * [1]Normal, no any error and warning. * [2]Warn, no any error but some warnings raise. * [3]Error, some errors raise. * 6. Verifier JDK: The JDK version that verifies signed jars. * 7. Status of Verifying: Verifying process result status. The status * are the same as those for "Status of Signing". * 8. Failed: It highlights which case fails. The failed cases (rows) * are marked with X. * * @modules java.base/sun.security.pkcs * java.base/sun.security.timestamp * java.base/sun.security.tools.keytool * java.base/sun.security.util * java.base/sun.security.x509 * @library /test/lib /lib/testlibrary ../warnings * @run main/manual/othervm Compatibility */ import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import jdk.test.lib.process.OutputAnalyzer; import jdk.test.lib.process.ProcessTools; import jdk.test.lib.util.JarUtils; public class Compatibility { private static final String TEST_FILE_NAME = "test"; private static final String TEST_JAR_NAME = "test.jar"; private static final String TEST_SRC = System.getProperty("test.src"); private static final String TEST_JDK = System.getProperty("test.jdk"); private static final String TEST_JARSIGNER = jarsignerPath(TEST_JDK); // If true, the test has to check whether the default timestamp digest // algorithm is SHA-256. And it doesn't allow any verification failure even // if the jarsigner doesn't support a specific algorithm. private static final boolean STRICT = Boolean.parseBoolean( System.getProperty("strict", "true")); private static final String PROXY_HOST = System.getProperty("proxyHost"); private static final String PROXY_PORT = System.getProperty("proxyPort", "80"); private static final String TSA = System.getProperty("tsa"); // An alternative security properties file, which only contains two lines: // jdk.certpath.disabledAlgorithms=MD2, MD5 // jdk.jar.disabledAlgorithms=MD2, MD5 private static final String JAVA_SECURITY = TEST_SRC + "/java.security"; private static final String ALIAS_PRE = "test"; private static final String PASSWORD = "testpass"; private static final String KEYSTORE = "testKeystore"; private static final String RSA = "RSA"; private static final String DSA = "DSA"; private static final String DEFAULT = "DEFAULT"; private static final String[] SIGNATURE_ALGORITHMS = new String[] { "SHA1withRSA", "SHA256withRSA", "SHA1withDSA", "SHA256withDSA" }; private static final String[] TSA_DIGEST_ALGORITHMS = new String[] { DEFAULT, "SHA-1", "SHA-256" }; private static final String VALID_CERT = "Valid"; private static final String EXPIRED_CERT = "Expired"; private static final String[] CERT_TYPES = new String[] { VALID_CERT, EXPIRED_CERT }; private static final Map CERTS = new HashMap(); private static final List JDK_INFOS = new ArrayList(); private static final List SIGN_ITEMS = new ArrayList(); // Create a jar file that contains an empty file. private static void createJar() throws IOException { new File(TEST_FILE_NAME).createNewFile(); JarUtils.createJar(TEST_JAR_NAME, TEST_FILE_NAME); } // Create/Update a key store that adds a certificate with specific algorithm. private static void createCertificate(String keyAlgorithm, String certType) throws Throwable { String nameSuffix = certType + keyAlgorithm; String alias = ALIAS_PRE + nameSuffix; OutputAnalyzer outputAnalyzer = exec( TEST_JDK + "/bin/keytool", "-J-Djava.security.properties=" + JAVA_SECURITY, "-v", "-storetype", "jks", "-genkey", "-keyalg", keyAlgorithm, "-sigalg", "SHA1with" + keyAlgorithm, "-keysize", "1024", "-dname", "CN=Test" + nameSuffix, "-alias", alias, "-keypass", PASSWORD, "-storepass", PASSWORD, "-startdate", "-2d", "-validity", certType == EXPIRED_CERT ? "1" : "3650", "-keystore", KEYSTORE); if (outputAnalyzer.getExitValue() == 0 && !outputAnalyzer.getOutput().matches("[Ee]xception")) { CERTS.put(new CertType(keyAlgorithm, certType), alias); } } public static void main(String[] args) throws Throwable { if (TSA == null || TSA.isEmpty()) { throw new RuntimeException("TSA service is mandatory."); } // Redirects the output to a file, named test.out. PrintStream out = new PrintStream( new FileOutputStream("test.out", true)); System.setOut(out); System.setErr(out); createJar(); // Create a key store that includes valid/expired RSA/DSA certificates. createCertificate(RSA, VALID_CERT); createCertificate(RSA, EXPIRED_CERT); createCertificate(DSA, VALID_CERT); createCertificate(DSA, EXPIRED_CERT); String[] jdkPaths = jdkList(); signJarByJDKs(jdkPaths); List reportItems = verifyJarByJDKs(jdkPaths); boolean failed = generateReport(reportItems); if (failed) { throw new RuntimeException("Test failed. " + "Please check the failed row(s) in testReport " + "and more details in test.out."); } } // Retrieves JDK paths from the file which is specified by property jdkListFile, // or from property jdkList if jdkListFile is not available. private static String[] jdkList() throws IOException { String jdkListFile = System.getProperty("jdkListFile"); if (jdkListFile != null) { System.out.println("JDK List file: " + jdkListFile); List jdkPaths = new ArrayList(); BufferedReader reader = new BufferedReader( new FileReader(jdkListFile)); String line; while ((line = reader.readLine()) != null) { String jdkPath = line.trim(); if (!jdkPath.isEmpty()) { jdkPaths.add(jdkPath); } } reader.close(); return jdkPaths.toArray(new String[0]); } String jdkList = System.getProperty("jdkList", TEST_JDK); System.out.println("JDK List:\n" + jdkList); String[] jdkPaths = jdkList.split(","); return jdkPaths; } private static void signJarByJDKs(String[] jdkPaths) throws Throwable { for (String signerJdkPath : jdkPaths) { JdkInfo jdkInfo = new JdkInfo(signerJdkPath); JDK_INFOS.add(jdkInfo); for (String sigAlg : SIGNATURE_ALGORITHMS) { for (String tsaDigest : TSA_DIGEST_ALGORITHMS) { // If the JDK doesn't support option -tsadigestalg, // the associated cases just be ignored. if (tsaDigest != DEFAULT && !jdkInfo.supportsTsadigestalg) { continue; } for (String certType : CERT_TYPES) { String alias = CERTS.get(new CertType( sigAlg.contains(RSA) ? RSA : DSA, certType)); String signedJarPath = jdkInfo.version + "_" + sigAlg + "_" + tsaDigest + "_" + certType + ".jar"; String tsadigestalg = default2Null(tsaDigest); String signOutput = signJar(jdkInfo.jarsignerPath, sigAlg, tsadigestalg, alias, signedJarPath); STATUS status = signStatus(signOutput); SignItem signItem = null; if (status != STATUS.ERROR) { signItem = SignItem.build() .version(jdkInfo.version) .signatureAlgorithm(sigAlg) .tsaDigestAlgorithm(tsadigestalg) .certType(certType).status(status) .signedJarPath(signedJarPath); SIGN_ITEMS.add(signItem); // Using the testing JDK, which is specified by // jtreg option "-jdk", to verify the signed jar // and extract the timestamp digest algorithm. String verifyOutput = verifyJar(TEST_JARSIGNER, signedJarPath); signItem.extractedTsaDigestAlgorithm( extract(verifyOutput, " *Timestamp digest algorithm.*", ".*(: )| \\(.*")); } else { jdkInfo.addUnsupportedSigAlg(sigAlg); } } } } } } private static List verifyJarByJDKs(String[] jdkPaths) throws Throwable { List reportItems = new ArrayList(); for (String verifierJdkPath : jdkPaths) { JdkInfo verifierJdkInfo = JDK_INFOS.get( JDK_INFOS.indexOf(new JdkInfo(verifierJdkPath))); for (String sigalg : SIGNATURE_ALGORITHMS) { for (String tsaDigest : TSA_DIGEST_ALGORITHMS) { // If property strict is false and the JDK doesn't support // the signature algorithm, then the case just be ignored. if (!STRICT && verifierJdkInfo.unsupportedSigAlgs.contains(sigalg)) { continue; } for (String certType : CERT_TYPES) { for (String signerJdkPath : jdkPaths) { SignItem signItem = findSignItem(SIGN_ITEMS, signerJdkPath, sigalg, tsaDigest, certType); // If signItem is null, it means the signing process // failed, so there is no associated signed jar. Then // it cannot do verifying. if (signItem != null) { String verifyOutput = verifyJar( verifierJdkInfo.jarsignerPath, signItem.signedJarPath); STATUS verifyStatus = verifyStatus( verifyOutput); if (verifyStatus != STATUS.ERROR) { verifyStatus = (STRICT && signItem.tsaDigestAlgorithm == DEFAULT && signItem.extractedTsaDigestAlgorithm != null && !signItem.extractedTsaDigestAlgorithm .matches("SHA-?256")) ? STATUS.ERROR : verifyStatus; } VerifyItem verifyItem = VerifyItem.build() .version(verifierJdkInfo.version) .status(verifyStatus); ReportItem reportItem = new ReportItem(signItem, verifyItem); reportItems.add(reportItem); System.out.println("Result:\n" + reportItem); } } } } } } return reportItems; } // Finds a SignItem by the specified JDK, algorithms and certificate. private static SignItem findSignItem(List signItems, String signerJDKPath, String sigalg, String tsaDigest, String certType) throws Throwable { SignItem dummySignItem = SignItem.build() .version(javaVersion(signerJDKPath)) .signatureAlgorithm(sigalg) .tsaDigestAlgorithm(default2Null(tsaDigest)) .certType(certType); int index = signItems.indexOf(dummySignItem); return index != -1 ? signItems.get(index) : null; } private static String default2Null(String tsaDigest) { return !DEFAULT.equals(tsaDigest) ? tsaDigest : null; } // Determines the status of signing. private static STATUS signStatus(String output) { if (output.contains(Test.JAR_SIGNED)) { if (output.contains(Test.HAS_EXPIRED_CERT_SIGNING_WARNING)) { return STATUS.WARNING; } else { return STATUS.NORMAL; } } else { return STATUS.ERROR; } } // Determines the status of verifying. private static STATUS verifyStatus(String output) { if (output.contains(Test.JAR_VERIFIED)) { if (output.contains(Test.WARNING)) { return STATUS.WARNING; } else { return STATUS.NORMAL; } } else { return STATUS.ERROR; } } // Extracts string from text by specified patterns. private static String extract(String text, String linePattern, String replacePattern) { Matcher lineMatcher = Pattern.compile(linePattern).matcher(text); if (lineMatcher.find()) { String line = lineMatcher.group(0); return line.replaceAll(replacePattern, ""); } else { return null; } } // Extracts build version from java version info. private static String javaVersion(String jdkPath) throws Throwable { OutputAnalyzer outputAnalyzer = ProcessTools.executeCommand( jdkPath + "/bin/java", "-version"); String version = extract(outputAnalyzer.getOutput(), "Java\\(TM\\) SE Runtime Environment.*", "(.*\\(.* )|\\)"); return version != null ? version : "N/A"; } // Checks if the jarsigner supports option -tsadigestalg. private static boolean supportsTsadigestalg(String jarsignerPath) throws Throwable { OutputAnalyzer outputAnalyzer = exec(jarsignerPath, "-help"); return outputAnalyzer.getOutput().contains("-tsadigestalg"); } // Using specified jarsigner to sign the pre-created jar with specified // algorithms. private static String signJar(String jarsignerPath, String sigalg, String tsadigestalg, String alias, String signedJarPath) throws Throwable { List arguments = new ArrayList(); if (PROXY_HOST != null && PROXY_PORT != null) { arguments.add("-J-Dhttp.proxyHost=" + PROXY_HOST); arguments.add("-J-Dhttp.proxyPort=" + PROXY_PORT); arguments.add("-J-Dhttps.proxyHost=" + PROXY_HOST); arguments.add("-J-Dhttps.proxyPort=" + PROXY_PORT); } arguments.add("-J-Djava.security.properties=" + JAVA_SECURITY); arguments.add("-verbose"); if (sigalg != null) { arguments.add("-sigalg"); arguments.add(sigalg); } arguments.add("-tsa"); arguments.add(TSA); if (tsadigestalg != null) { arguments.add("-tsadigestalg"); arguments.add(tsadigestalg); } arguments.add("-keystore"); arguments.add(KEYSTORE); arguments.add("-storepass"); arguments.add(PASSWORD); arguments.add("-signedjar"); arguments.add(signedJarPath); arguments.add(TEST_JAR_NAME); arguments.add(alias); OutputAnalyzer outputAnalyzer = exec( jarsignerPath, arguments.toArray(new String[arguments.size()])); return outputAnalyzer.getOutput(); } // Using specified jarsigner to verify the signed jar. private static String verifyJar(String jarsignerPath, String signedJarPath) throws Throwable { OutputAnalyzer outputAnalyzer = exec( jarsignerPath, "-J-Djava.security.properties=" + JAVA_SECURITY, "-verbose", "-certs", "-keystore", KEYSTORE, "-verify", signedJarPath); return outputAnalyzer.getOutput(); } // Generates the test result report. private static boolean generateReport(List reportItems) throws IOException { System.out.println("Report is being generated..."); StringBuilder report = new StringBuilder(); // Generates report headers. report.append(ReportHeader.HEADERS).append("\n"); boolean failed = false; // Generates report rows. for (ReportItem item : reportItems) { failed = failed || item.verifyItem.status == STATUS.ERROR; report.append(item).append("\n"); } FileWriter writer = new FileWriter(new File("testReport")); writer.write(report.toString()); writer.close(); System.out.println("Report is generated."); return failed; } private static String jarsignerPath(String jdkPath) { return jdkPath + "/bin/jarsigner"; } // Executes command against the specified JDK tool, and ensures the output // is US English. private static OutputAnalyzer exec(String toolPath, String... args) throws Throwable { String[] cmd = new String[args.length + 3]; cmd[0] = toolPath; cmd[1] = "-J-Duser.language=en"; cmd[2] = "-J-Duser.country=US"; System.arraycopy(args, 0, cmd, 3, args.length); return ProcessTools.executeCommand(cmd); } private static class JdkInfo { private final String jdkPath; private final String jarsignerPath; private final String version; private final boolean supportsTsadigestalg; private Set unsupportedSigAlgs = new HashSet(); private JdkInfo(String jdkPath) throws Throwable { this.jdkPath = jdkPath; version = javaVersion(jdkPath); jarsignerPath = jarsignerPath(jdkPath); supportsTsadigestalg = supportsTsadigestalg(jarsignerPath); } private void addUnsupportedSigAlg(String sigalg) { unsupportedSigAlgs.add(sigalg); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((jdkPath == null) ? 0 : jdkPath.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; JdkInfo other = (JdkInfo) obj; if (jdkPath == null) { if (other.jdkPath != null) return false; } else if (!jdkPath.equals(other.jdkPath)) return false; return true; } } private static class CertType { private final String keyAlgorithm; private final String certType; private CertType(String keyAlgorithm, String certType) { this.keyAlgorithm = keyAlgorithm; this.certType = certType; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((certType == null) ? 0 : certType.hashCode()); result = prime * result + ((keyAlgorithm == null) ? 0 : keyAlgorithm.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; CertType other = (CertType) obj; if (certType == null) { if (other.certType != null) return false; } else if (!certType.equals(other.certType)) return false; if (keyAlgorithm == null) { if (other.keyAlgorithm != null) return false; } else if (!keyAlgorithm.equals(other.keyAlgorithm)) return false; return true; } } private static enum STATUS { // Signing/Verifying with error ERROR, // jar is signed/verified with warning WARNING, // jar is signed/verified without any warning and error NORMAL } private static class SignItem { private String version; private String signatureAlgorithm; private String tsaDigestAlgorithm; // tsadigestalg that is extracted from verification output (if possible) private String extractedTsaDigestAlgorithm; private String certType; private STATUS status; private String signedJarPath; private static SignItem build() { return new SignItem(); } private SignItem version(String version) { this.version = version; return this; } private SignItem signatureAlgorithm(String signatureAlgorithm) { this.signatureAlgorithm = signatureAlgorithm; return this; } private SignItem tsaDigestAlgorithm(String tsaDigestAlgorithm) { this.tsaDigestAlgorithm = tsaDigestAlgorithm; return this; } private SignItem extractedTsaDigestAlgorithm( String extractedTsaDigestAlgorithm) { this.extractedTsaDigestAlgorithm = extractedTsaDigestAlgorithm; return this; } private SignItem certType(String certType) { this.certType = certType; return this; } private SignItem status(STATUS status) { this.status = status; return this; } private SignItem signedJarPath(String signedJarPath) { this.signedJarPath = signedJarPath; return this; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((certType == null) ? 0 : certType.hashCode()); result = prime * result + ((signatureAlgorithm == null) ? 0 : signatureAlgorithm.hashCode()); result = prime * result + ((tsaDigestAlgorithm == null) ? 0 : tsaDigestAlgorithm.hashCode()); result = prime * result + ((version == null) ? 0 : version.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; SignItem other = (SignItem) obj; if (certType == null) { if (other.certType != null) return false; } else if (!certType.equals(other.certType)) return false; if (signatureAlgorithm == null) { if (other.signatureAlgorithm != null) return false; } else if (!signatureAlgorithm.equals(other.signatureAlgorithm)) return false; if (tsaDigestAlgorithm == null) { if (other.tsaDigestAlgorithm != null) return false; } else if (!tsaDigestAlgorithm.equals(other.tsaDigestAlgorithm)) return false; if (version == null) { if (other.version != null) return false; } else if (!version.equals(other.version)) return false; return true; } } private static class VerifyItem { private String version; private STATUS status; private static VerifyItem build() { return new VerifyItem(); } private VerifyItem version(String version) { this.version = version; return this; } private VerifyItem status(STATUS status) { this.status = status; return this; } } private static class ReportHeader { // Header names private static final String SIGNER_VERSION = "[Signer JDK]"; private static final String SIGNATURE_ALGORITHM = "[Signature Algorithm]"; private static final String TSA_DIGEST_ALGORITHM = "[TSA Digest Algorithm]"; private static final String CERT_TYPE = "[Cert Type]"; private static final String SIGNE_STATUS = "[Status of Signing]"; private static final String VERIFIER_VERSION = "[Verifier JDK]"; private static final String VERIFY_STATUS = "[Status of Verifying]"; private static final String FAILED = "[Failed]"; // Header widths private static final int W_SIGNER_VERSION = 16; private static final int W_SIGNATURE_ALGORITHM = 23; private static final int W_TSA_DIGEST_ALGORITHM = TSA_DIGEST_ALGORITHM.length(); private static final int W_CERT_TYPE = CERT_TYPE.length(); private static final int W_SIGNED = SIGNE_STATUS.length(); private static final int W_VERIFIER_VERSION = W_SIGNER_VERSION; private static final int W_VERIFY_STATUS = VERIFY_STATUS.length(); private static final int W_FAILED = FAILED.length(); private static final String DELIMITER = " "; private static final String FORMAT = "%-" + W_SIGNER_VERSION + "s" + DELIMITER + "%-" + W_SIGNATURE_ALGORITHM + "s" + DELIMITER + "%-" + W_TSA_DIGEST_ALGORITHM + "s" + DELIMITER + "%-" + W_CERT_TYPE + "s" + DELIMITER + "%-" + W_SIGNED + "s" + DELIMITER + "%-" + W_VERIFIER_VERSION + "s" + DELIMITER + "%-" + W_VERIFY_STATUS + "s" + DELIMITER + "%-" + W_FAILED + "s"; private static final String HEADERS = String.format( ReportHeader.FORMAT, SIGNER_VERSION, SIGNATURE_ALGORITHM, TSA_DIGEST_ALGORITHM, CERT_TYPE, SIGNE_STATUS, VERIFIER_VERSION, VERIFY_STATUS, FAILED); } private static class ReportItem { private final SignItem signItem; private final VerifyItem verifyItem; private ReportItem(SignItem signItem, VerifyItem verifyItem) { this.signItem = signItem; this.verifyItem = verifyItem; } @Override public String toString() { return String.format(ReportHeader.FORMAT, signItem.version, signItem.signatureAlgorithm, null2Default(signItem.tsaDigestAlgorithm, signItem.extractedTsaDigestAlgorithm), signItem.certType, signItem.status, verifyItem.version, verifyItem.status, verifyItem.status == STATUS.ERROR ? "X" : ""); } // If a value is null, then displays the default value or N/A. private static String null2Default(String value, String defaultValue) { return value == null ? DEFAULT + "(" + (defaultValue == null ? "N/A" : defaultValue) + ")" : value; } } }