1 /*
   2  * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 /*
  25  * @test
  26  * @bug 8177674
  27  * @summary This test is used to verify the compatibility on jarsigner cross
  28  *     different JDK releases. If property strict is true, it also checks whether
  29  *     the default timestamp digest algorithm is SHA-256, and all of verification
  30  *     must pass even if the JDK build doesn't support some specific algorithms.
  31  *
  32  *     The test will generate a report, at JTwork/scratch/testReport, to display
  33  *     the key parameters for signing and the status of signing and verifying.
  34  *
  35  *     Please note that, the test may output a great deal of logs if the jdk list
  36  *     is big, and that would lead to jtreg output overflow. So, it redirects
  37  *     the stdout and stderr to file JTwork/scratch/test.out.
  38  *
  39  *     The testing JDK, which is specified by jtreg option "-jdk", should include
  40  *     the fix for JDK-8163304. Otherwise, the signature algorithm and timestamp
  41  *     digest algorithm cannot be extracted from verification output. And this
  42  *     JDK should support as many as possible signature algorithms. So, the latest
  43  *     JDK build is recommended.
  44  *
  45  *     Usage: jtreg [-options] \
  46  *                  [-Dstrict=<true|false> \
  47  *                   -DproxyHost=<host> -DproxyPort=<port> \
  48  *                   -Dtsa=</url/to/TSA/service> \
  49  *                   -DjdkListFile=</path/to/jdkListFile> \
  50  *                   -DjdkList=</path/to/jdk1,/path/to/jdk2,/path/to/jdk3,...> \
  51  *                   -DjavaSecurityFile=</path/to/java/security/properties/file>] \
  52  *                  /path/to/Compatibility.java
  53  *
  54  *     Properties:
  55  *         1. strict=<true|false>
  56  *            If true, the test checks whether the default timestamp digest
  57  *            algorithm is SHA-256, and all of verification must pass even if
  58  *            the JDK build doesn't support a specific algorithm. The default
  59  *            value is true.
  60  *
  61  *         2. proxyHost=<host>
  62  *            This property indicates proxy host.
  63  *
  64  *         3. proxyPort=<port>
  65  *            This property indicates proxy port. The default value is 80.
  66  *
  67  *         4. tsa=</url/to/TSA/service>
  68  *            This property indicates a TSA service. It is mandatory.
  69  *
  70  *         5. jdkListFile=</path/to/jdkListFile>
  71  *            This property indicates a local file, which contains a set of local
  72  *            JDK paths. The style of the file content looks like the below,
  73  *            /path/to/jdk1
  74  *            /path/to/jdk2
  75  *            /path/to/jdk3
  76  *            ...
  77  *
  78  *         6. jdkList=</path/to/jdk1,/path/to/jdk2,/path/to/jdk3,...>
  79  *            This property directly lists a set of local JDK paths in command.
  80  *            Note that, if both of jdkListFile and jdkList are specified, only
  81  *            property jdkListFile is selected. If neither of jdkListFile and
  82  *            jdkList is specified, the testing JDK, which is specified by jtreg
  83  *            option -jdk will be used as the only one JDK in the JDK list.
  84  *
  85  *         7. javaSecurityFile=</path/to/java/security/properties/file>
  86  *            This property indicates an alternative java security properties
  87  *            file. The default value is the path of file java.scurity that is
  88  *            distributed with this test.
  89  *
  90  *     Report columns:
  91  *         1. Signer JDK: The JDK version that signs jar.
  92  *         2. Signature Algorithm: The signature algorithm used by signing.
  93  *         3. TSA Digest Algorithm: The timestamp digest algorithm used by signing.
  94  *         4. Certificate: Certificate identifier.
  95  *            The identifier consists of specific attributes of the certificate.
  96  *            The naming convention is:
  97  *            KeyAlgorithm_DigestAlgorithm_KeySize[_Expired]
  98  *         5. Status of Signing: Signing process result status.
  99  *            The status are the followings:
 100  *            [1]Normal, no any error and warning.
 101  *            [2]Warn, no any error but some warnings raise.
 102  *            [3]Error, some errors raise.
 103  *         6. Verifier JDK: The JDK version that verifies signed jars.
 104  *         7. Status of Verifying: Verifying process result status. The status
 105  *            are the same as those for "Status of Signing".
 106  *         8. Failed: It highlights which case fails. The failed cases (rows)
 107  *            are marked with letter X.
 108  *
 109  * @modules java.base/sun.security.pkcs
 110  *          java.base/sun.security.timestamp
 111  *          java.base/sun.security.tools.keytool
 112  *          java.base/sun.security.util
 113  *          java.base/sun.security.x509
 114  * @library /test/lib /lib/testlibrary ../warnings
 115  * @compile -source 1.6 -target 1.6 JdkUtils.java
 116  * @run main/manual/othervm Compatibility
 117  */
 118 
 119 import java.io.BufferedReader;
 120 import java.io.File;
 121 import java.io.FileOutputStream;
 122 import java.io.FileReader;
 123 import java.io.FileWriter;
 124 import java.io.IOException;
 125 import java.io.PrintStream;
 126 import java.util.ArrayList;
 127 import java.util.HashMap;
 128 import java.util.List;
 129 import java.util.Map;
 130 import java.util.regex.Matcher;
 131 import java.util.regex.Pattern;
 132 
 133 import jdk.test.lib.process.OutputAnalyzer;
 134 import jdk.test.lib.process.ProcessTools;
 135 import jdk.test.lib.util.JarUtils;
 136 
 137 public class Compatibility {
 138 
 139     private static final String TEST_JAR_NAME = "test.jar";
 140 
 141     private static final String TEST_SRC = System.getProperty("test.src");
 142     private static final String TEST_CLASSES = System.getProperty("test.classes");
 143     private static final String TEST_JDK = System.getProperty("test.jdk");
 144     private static final String TEST_JARSIGNER = jarsignerPath(TEST_JDK);
 145 
 146     // If true, the test has to check whether the default timestamp digest
 147     // algorithm is SHA-256. And it doesn't allow any verification failure even
 148     // if the jarsigner doesn't support a specific algorithm.
 149     private static final boolean STRICT = Boolean.parseBoolean(
 150             System.getProperty("strict", "true"));
 151 
 152     private static final String PROXY_HOST = System.getProperty("proxyHost");
 153     private static final String PROXY_PORT = System.getProperty("proxyPort", "80");
 154     private static final String TSA = System.getProperty("tsa");
 155 
 156     // An alternative security properties file.
 157     // The test provides a default one, which only contains two lines:
 158     // jdk.certpath.disabledAlgorithms=MD2, MD5
 159     // jdk.jar.disabledAlgorithms=MD2, MD5
 160     private static final String JAVA_SECURITY = System.getProperty(
 161             "javaSecurityFile", TEST_SRC + "/java.security");
 162 
 163     private static final String PASSWORD = "testpass";
 164     private static final String KEYSTORE = "testKeystore";
 165 
 166     private static final String DEFAULT = "DEFAULT";
 167     private static final String RSA = "RSA";
 168     private static final String DSA = "DSA";
 169     private static final String EC = "EC";
 170     private static final String[] KEY_ALGORITHMS = new String[] {
 171             RSA,
 172             DSA,
 173             EC};
 174 
 175     private static final String SHA1 = "SHA-1";
 176     private static final String SHA256 = "SHA-256";
 177     private static final String SHA512 = "SHA-512";
 178     private static final String[] DIGEST_ALGORITHMS = new String[] {
 179             SHA1,
 180             SHA256,
 181             SHA512,
 182             DEFAULT};
 183 
 184     private static final boolean[] EXPIRED = new boolean[] {
 185             false,
 186             true};
 187 
 188     public static void main(String[] args) throws Throwable {
 189         if (TSA == null || TSA.isEmpty()) {
 190             throw new RuntimeException("TSA service is mandatory.");
 191         }
 192 
 193         // Redirects the output to a file, named test.out.
 194         PrintStream out = new PrintStream(
 195                 new FileOutputStream("test.out", true));
 196         System.setOut(out);
 197         System.setErr(out);
 198 
 199         createJar();
 200         List<ReportItem> reportItems = test(jdkList(), createCertificates());
 201         boolean failed = generateReport(reportItems);
 202 
 203         if (failed) {
 204             throw new RuntimeException("Test failed. "
 205                     + "Please check the failed row(s) in testReport "
 206                     + "and more details in test.out.");
 207         }
 208     }
 209 
 210     // Creates a jar file that contains an empty file.
 211     private static void createJar() throws IOException {
 212         String testFile = "test";
 213         new File(testFile).createNewFile();
 214         JarUtils.createJar(TEST_JAR_NAME, testFile);
 215     }
 216 
 217     // Creates a key store that includes a set of (expired) certificates with
 218     // various algorithms.
 219     private static Map<CertInfo, String> createCertificates() throws Throwable {
 220         Map<CertInfo, String> certs = new HashMap<CertInfo, String>();
 221 
 222         for(String keyAlgorithm : KEY_ALGORITHMS) {
 223             for(String digestAlgorithm : DIGEST_ALGORITHMS) {
 224                 if (DEFAULT.equals(digestAlgorithm)) {
 225                     continue;
 226                 }
 227 
 228                 for(int keySize : keySizes(keyAlgorithm)) {
 229                     for(boolean expired : EXPIRED) {
 230                         CertInfo certInfo = new CertInfo(
 231                                 keyAlgorithm,
 232                                 digestAlgorithm,
 233                                 keySize,
 234                                 expired);
 235                         String alias = createCertificate(certInfo);
 236                         certs.put(certInfo, alias);
 237                     }
 238                 }
 239             }
 240         }
 241 
 242         return certs;
 243     }
 244 
 245     // Creates/Updates a key store that adds a certificate with specific algorithm.
 246     private static String createCertificate(CertInfo certInfo) throws Throwable {
 247         String alias = certInfo.toString();
 248 
 249         OutputAnalyzer outputAnalyzer = execTool(
 250                 TEST_JDK + "/bin/keytool",
 251                 "-J-Djava.security.properties=" + JAVA_SECURITY,
 252                 "-v",
 253                 "-storetype", "jks",
 254                 "-genkey",
 255                 "-keyalg", certInfo.keyAlgorithm,
 256                 "-sigalg", sigalg(certInfo.digestAlgorithm, certInfo.keyAlgorithm),
 257                 "-keysize", certInfo.keySize + "",
 258                 "-dname", "CN=" + alias,
 259                 "-alias", alias,
 260                 "-keypass", PASSWORD,
 261                 "-storepass", PASSWORD,
 262                 "-startdate", "-2d",
 263                 "-validity", certInfo.expired ? "1" : "3650",
 264                 "-keystore", KEYSTORE);
 265         if (outputAnalyzer.getExitValue() == 0
 266                 && !outputAnalyzer.getOutput().matches("[Ee]xception")) {
 267             return alias;
 268         } else {
 269             return null;
 270         }
 271     }
 272 
 273     private static String sigalg(String digestAlgorithm, String keyAlgorithm) {
 274         if (digestAlgorithm == DEFAULT) {
 275             return null;
 276         }
 277 
 278         String keyName = EC.equals(keyAlgorithm) ? "ECDSA" : keyAlgorithm;
 279         return digestAlgorithm.replace("-", "") + "with" + keyName;
 280     }
 281 
 282     // Retrieves JDK paths from the file which is specified by property jdkListFile,
 283     // or from property jdkList if jdkListFile is not available.
 284     private static String[] jdkList() throws IOException {
 285         String jdkListFile = System.getProperty("jdkListFile");
 286         if (jdkListFile != null) {
 287             System.out.println("JDK List file: " + jdkListFile);
 288             List<String> jdkPaths = new ArrayList<String>();
 289             BufferedReader reader = new BufferedReader(
 290                     new FileReader(jdkListFile));
 291             String line;
 292             while ((line = reader.readLine()) != null) {
 293                 String jdkPath = line.trim();
 294                 if (!jdkPath.isEmpty()) {
 295                     jdkPaths.add(jdkPath);
 296                 }
 297             }
 298             reader.close();
 299             return jdkPaths.toArray(new String[jdkPaths.size()]);
 300         }
 301 
 302         String jdkList = System.getProperty("jdkList", TEST_JDK);
 303         System.out.println("JDK List:\n" + jdkList);
 304         String[] jdkPaths = jdkList.split(",");
 305         return jdkPaths;
 306     }
 307 
 308     // A JDK (signer) signs a jar with a variety of algorithms, and then all of
 309     // JDKs (verifiers), including the signer itself, try to verify the signed
 310     // jars respectively.
 311     private static List<ReportItem> test(String[] jdkPaths,
 312             Map<CertInfo, String> certs) throws Throwable {
 313         List<ReportItem> reportItems = new ArrayList<ReportItem>();
 314 
 315         for (String signerPath : jdkPaths) {
 316             JdkInfo signerInfo = JdkInfoMap.getJdkInfo(signerPath);
 317 
 318             for (String keyAlgorithm : KEY_ALGORITHMS) {
 319                 for (String digestAlgorithm : DIGEST_ALGORITHMS) {
 320                     String sigalg = sigalg(digestAlgorithm, keyAlgorithm);
 321                     // If the signature algorithm is not supported by the JDK,
 322                     // it cannot try to sign jar with this algorithm.
 323                     if (sigalg != null && !signerInfo.isSupportedSigalg(sigalg)) {
 324                         continue;
 325                     }
 326 
 327                     // If the JDK doesn't support option -tsadigestalg, the
 328                     // associated cases just be ignored.
 329                     if (digestAlgorithm != DEFAULT
 330                             && !signerInfo.supportsTsadigestalg) {
 331                         continue;
 332                     }
 333 
 334                     for (int keySize : keySizes(keyAlgorithm)) {
 335                         for (boolean expired : EXPIRED) {
 336                             String certDigest = digestAlgorithm;
 337                             int certKeySize = keySize;
 338                             // If the digest algorithm is not specified, then it
 339                             // uses certificate with SHA256 digest and 1024 key
 340                             // size.
 341                             if (digestAlgorithm == DEFAULT) {
 342                                 certDigest = SHA256;
 343                                 certKeySize = 1024;
 344                             }
 345 
 346                             CertInfo certInfo = new CertInfo(
 347                                     keyAlgorithm,
 348                                     certDigest,
 349                                     certKeySize,
 350                                     expired);
 351                             String alias = certs.get(certInfo);
 352                             if (alias == null) {
 353                                 continue;
 354                             }
 355 
 356                             String signedJarPath = signerInfo.version + "_"
 357                                     + certInfo + ".jar";
 358                             String tsadigestalg = digestAlgorithm != DEFAULT
 359                                     ? digestAlgorithm : null;
 360 
 361                             String signOutput = signJar(
 362                                     signerInfo.jarsignerPath,
 363                                     sigalg,
 364                                     tsadigestalg,
 365                                     alias,
 366                                     signedJarPath);
 367                             STATUS signingStatus = signingStatus(signOutput);
 368 
 369                             // If signing fails, the following verifying has to
 370                             // be ignored.
 371                             if (signingStatus == STATUS.ERROR) {
 372                                 continue;
 373                             }
 374 
 375                             SignItem signItem = SignItem.build()
 376                                     .version(signerInfo.version)
 377                                     .signatureAlgorithm(sigalg)
 378                                     .tsaDigestAlgorithm(tsadigestalg)
 379                                     .certInfo(certInfo)
 380                                     .status(signingStatus)
 381                                     .signedJarPath(signedJarPath);
 382 
 383                             // Using the testing JDK, which is specified by jtreg
 384                             // option "-jdk", to verify the signed jar and extract
 385                             // the signature algorithm and timestamp digest algorithm.
 386                             String output = verifyJar(TEST_JARSIGNER,
 387                                     signedJarPath);
 388                             signItem.extractedSignatureAlgorithm(extract(output,
 389                                     " *Signature algorithm.*", ".*: |,.*"));
 390                             signItem.extractedTsaDigestAlgorithm(extract(output,
 391                                     " *Timestamp digest algorithm.*", ".*: "));
 392 
 393                             reportItems.addAll(
 394                                     verifyJarByJDKs(jdkPaths, signItem));
 395                         }
 396                     }
 397                 }
 398             }
 399         }
 400 
 401         return reportItems;
 402     }
 403 
 404     private static List<ReportItem> verifyJarByJDKs(String[] jdkPaths,
 405             SignItem signItem) throws Throwable {
 406         List<ReportItem> reportItems = new ArrayList<ReportItem>();
 407         for (String verifierPath : jdkPaths) {
 408             JdkInfo verifierInfo = JdkInfoMap.getJdkInfo(verifierPath);
 409             // If strict is false and the verifier JDK doesn't support this
 410             // signature algorithm, the case just be ignored.
 411             if (!STRICT
 412                     && signItem.signatureAlgorithm != null
 413                     && !verifierInfo.isSupportedSigalg(
 414                             signItem.signatureAlgorithm)) {
 415                 continue;
 416             }
 417 
 418             VerifyItem verifyItem = verifyJarByJDK(verifierInfo, signItem);
 419             ReportItem reportItem = new ReportItem(signItem, verifyItem);
 420             reportItems.add(reportItem);
 421             System.out.println("Result:\n" + reportItem);
 422         }
 423         return reportItems;
 424     }
 425 
 426     private static VerifyItem verifyJarByJDK(JdkInfo verifierInfo,
 427             SignItem signItem) throws Throwable {
 428         String verifyOutput = verifyJar(verifierInfo.jarsignerPath,
 429                 signItem.signedJarPath);
 430         STATUS verifyingStatus = verifyingStatus(verifyOutput);
 431 
 432         // If strict is true, it has to check if the default timestamp digest
 433         // algorithm is SHA-256.
 434         if (verifyingStatus != STATUS.ERROR) {
 435             verifyingStatus = (STRICT && signItem.tsaDigestAlgorithm == DEFAULT
 436                     && signItem.extractedTsaDigestAlgorithm != null
 437                     && !signItem.extractedTsaDigestAlgorithm
 438                             .matches("SHA-?256"))
 439                                     ? STATUS.ERROR
 440                                     : verifyingStatus;
 441         }
 442 
 443         VerifyItem verifyItem = VerifyItem.build()
 444                 .version(verifierInfo.version)
 445                 .status(verifyingStatus);
 446         return verifyItem;
 447     }
 448 
 449     // Return key sizes according to the specified key algorithm.
 450     private static int[] keySizes(String keyAlgorithm) {
 451         if (keyAlgorithm == RSA || keyAlgorithm == DSA) {
 452             return new int[] { 1024, 2048 };
 453         } else if (keyAlgorithm == EC) {
 454             return new int[] { 384, 571 };
 455         }
 456 
 457         return null;
 458     }
 459 
 460     // Determines the status of signing.
 461     private static STATUS signingStatus(String output) {
 462         if (output.contains(Test.JAR_SIGNED)) {
 463             if (output.contains(Test.HAS_EXPIRED_CERT_SIGNING_WARNING)) {
 464                 return STATUS.WARNING;
 465             } else {
 466                 return STATUS.NORMAL;
 467             }
 468         } else {
 469             return STATUS.ERROR;
 470         }
 471     }
 472 
 473     // Determines the status of verifying.
 474     private static STATUS verifyingStatus(String output) {
 475         if (output.contains(Test.JAR_VERIFIED)) {
 476             if (output.contains(Test.WARNING)) {
 477                 return STATUS.WARNING;
 478             } else {
 479                 return STATUS.NORMAL;
 480             }
 481         } else {
 482             return STATUS.ERROR;
 483         }
 484     }
 485 
 486     // Extracts string from text by specified patterns.
 487     private static String extract(String text, String linePattern,
 488             String replacePattern) {
 489         Matcher lineMatcher = Pattern.compile(linePattern).matcher(text);
 490         if (lineMatcher.find()) {
 491             String line = lineMatcher.group(0);
 492             return line.replaceAll(replacePattern, "");
 493         } else {
 494             return null;
 495         }
 496     }
 497 
 498     // Using specified jarsigner to sign the pre-created jar with specified
 499     // algorithms.
 500     private static String signJar(String jarsignerPath, String sigalg,
 501             String tsadigestalg, String alias, String signedJarPath)
 502             throws Throwable {
 503         List<String> arguments = new ArrayList<String>();
 504         if (PROXY_HOST != null && PROXY_PORT != null) {
 505             arguments.add("-J-Dhttp.proxyHost=" + PROXY_HOST);
 506             arguments.add("-J-Dhttp.proxyPort=" + PROXY_PORT);
 507             arguments.add("-J-Dhttps.proxyHost=" + PROXY_HOST);
 508             arguments.add("-J-Dhttps.proxyPort=" + PROXY_PORT);
 509         }
 510         arguments.add("-J-Djava.security.properties=" + JAVA_SECURITY);
 511         arguments.add("-verbose");
 512         if (sigalg != null) {
 513             arguments.add("-sigalg");
 514             arguments.add(sigalg);
 515         }
 516         arguments.add("-tsa");
 517         arguments.add(TSA);
 518         if (tsadigestalg != null) {
 519             arguments.add("-tsadigestalg");
 520             arguments.add(tsadigestalg);
 521         }
 522         arguments.add("-keystore");
 523         arguments.add(KEYSTORE);
 524         arguments.add("-storepass");
 525         arguments.add(PASSWORD);
 526         arguments.add("-signedjar");
 527         arguments.add(signedJarPath);
 528         arguments.add(TEST_JAR_NAME);
 529         arguments.add(alias);
 530 
 531         OutputAnalyzer outputAnalyzer = execTool(
 532                 jarsignerPath,
 533                 arguments.toArray(new String[arguments.size()]));
 534         return outputAnalyzer.getOutput();
 535     }
 536 
 537     // Using specified jarsigner to verify the signed jar.
 538     private static String verifyJar(String jarsignerPath, String signedJarPath)
 539             throws Throwable {
 540         OutputAnalyzer outputAnalyzer = execTool(
 541                 jarsignerPath,
 542                 "-J-Djava.security.properties=" + JAVA_SECURITY,
 543                 "-verbose", "-certs",
 544                 "-keystore", KEYSTORE,
 545                 "-verify", signedJarPath);
 546         return outputAnalyzer.getOutput();
 547     }
 548 
 549     // Generates the test result report.
 550     private static boolean generateReport(List<ReportItem> reportItems)
 551             throws IOException {
 552         System.out.println("Report is being generated...");
 553 
 554         StringBuilder report = new StringBuilder();
 555 
 556         // Generates report headers.
 557         report.append(ReportHeader.HEADERS).append("\n");
 558 
 559         boolean failed = false;
 560 
 561         // Generates report rows.
 562         for (ReportItem item : reportItems) {
 563             failed = failed || item.verifyItem.status == STATUS.ERROR;
 564             report.append(item).append("\n");
 565         }
 566 
 567         FileWriter writer = new FileWriter(new File("testReport"));
 568         writer.write(report.toString());
 569         writer.close();
 570 
 571         System.out.println("Report is generated.");
 572         return failed;
 573     }
 574 
 575     private static String jarsignerPath(String jdkPath) {
 576         return jdkPath + "/bin/jarsigner";
 577     }
 578 
 579     // Executes the specified function on JdkUtils by the specified JDK.
 580     private static String execJdkUtils(String jdkPath, String method,
 581             String... args) throws Throwable {
 582         String[] cmd = new String[args.length + 5];
 583         cmd[0] = jdkPath + "/bin/java";
 584         cmd[1] = "-cp";
 585         cmd[2] = TEST_CLASSES;
 586         cmd[3] = JdkUtils.class.getName();
 587         cmd[4] = method;
 588         System.arraycopy(args, 0, cmd, 5, args.length);
 589         return ProcessTools.executeCommand(cmd).getOutput();
 590     }
 591 
 592     // Executes the specified JDK tools, such as keytool and jarsigner, and
 593     // ensures the output is in US English.
 594     private static OutputAnalyzer execTool(String toolPath, String... args)
 595             throws Throwable {
 596         String[] cmd = new String[args.length + 4];
 597         cmd[0] = toolPath;
 598         cmd[1] = "-J-Duser.language=en";
 599         cmd[2] = "-J-Duser.country=US";
 600         cmd[3] = "-J-Djava.security.egd=file:/dev/./urandom";
 601         System.arraycopy(args, 0, cmd, 4, args.length);
 602         return ProcessTools.executeCommand(cmd);
 603     }
 604 
 605     private static class JdkInfoMap {
 606 
 607         private static Map<String, JdkInfo> map = new HashMap<String, JdkInfo>();
 608 
 609         private static JdkInfo getJdkInfo(String jdkPath) throws Throwable {
 610             if (!map.containsKey(jdkPath)) {
 611                 map.put(jdkPath, new JdkInfo(jdkPath));
 612             }
 613             return map.get(jdkPath);
 614         }
 615     }
 616 
 617     private static class JdkInfo {
 618 
 619         private final String jdkPath;
 620         private final String jarsignerPath;
 621         private final String version;
 622         private final boolean supportsTsadigestalg;
 623 
 624         private Map<String, Boolean> sigalgMap = new HashMap<String, Boolean>();
 625 
 626         private JdkInfo(String jdkPath) throws Throwable {
 627             this.jdkPath = jdkPath;
 628             version = execJdkUtils(jdkPath, JdkUtils.M_JAVA_RUNTIME_VERSION);
 629             if (version == null || version.trim().isEmpty()) {
 630                 throw new RuntimeException(
 631                         "Cannot determine the JDK version: " + jdkPath);
 632             }
 633             jarsignerPath = jarsignerPath(jdkPath);
 634             supportsTsadigestalg = execTool(jarsignerPath, "-help")
 635                     .getOutput().contains("-tsadigestalg");
 636         }
 637 
 638         private boolean isSupportedSigalg(String sigalg) throws Throwable {
 639             if (!sigalgMap.containsKey(sigalg)) {
 640                 boolean isSupported = "true".equalsIgnoreCase(
 641                         execJdkUtils(
 642                                 jdkPath,
 643                                 JdkUtils.M_IS_SUPPORTED_SIGALG,
 644                                 sigalg));
 645                 sigalgMap.put(sigalg, isSupported);
 646             }
 647 
 648             return sigalgMap.get(sigalg);
 649         }
 650     }
 651 
 652     private static class CertInfo {
 653 
 654         private final String keyAlgorithm;
 655         private final String digestAlgorithm;
 656         private final int keySize;
 657         private final boolean expired;
 658 
 659         private CertInfo(String keyAlgorithm, String digestAlgorithm,
 660                 int keySize, boolean expired) {
 661             this.keyAlgorithm = keyAlgorithm;
 662             this.digestAlgorithm = digestAlgorithm;
 663             this.keySize = keySize;
 664             this.expired = expired;
 665         }
 666 
 667         @Override
 668         public int hashCode() {
 669             final int prime = 31;
 670             int result = 1;
 671             result = prime * result + (expired ? 1231 : 1237);
 672             result = prime * result
 673                     + ((keyAlgorithm == null) ? 0 : keyAlgorithm.hashCode());
 674             result = prime * result + keySize;
 675             result = prime * result + ((digestAlgorithm == null) ? 0
 676                     : digestAlgorithm.hashCode());
 677             return result;
 678         }
 679 
 680         @Override
 681         public boolean equals(Object obj) {
 682             if (this == obj)
 683                 return true;
 684             if (obj == null)
 685                 return false;
 686             if (getClass() != obj.getClass())
 687                 return false;
 688             CertInfo other = (CertInfo) obj;
 689             if (expired != other.expired)
 690                 return false;
 691             if (keyAlgorithm == null) {
 692                 if (other.keyAlgorithm != null)
 693                     return false;
 694             } else if (!keyAlgorithm.equals(other.keyAlgorithm))
 695                 return false;
 696             if (keySize != other.keySize)
 697                 return false;
 698             if (digestAlgorithm == null) {
 699                 if (other.digestAlgorithm != null)
 700                     return false;
 701             } else if (!digestAlgorithm.equals(other.digestAlgorithm))
 702                 return false;
 703             return true;
 704         }
 705 
 706         @Override
 707         public String toString() {
 708             return keyAlgorithm + "_" + digestAlgorithm + "_" + keySize
 709                     + (expired ? "_Expired" : "");
 710         }
 711     }
 712 
 713     private static enum STATUS {
 714 
 715         // jar is signed/verified with error
 716         ERROR,
 717 
 718         // jar is signed/verified with warning
 719         WARNING,
 720 
 721         // jar is signed/verified without any warning and error
 722         NORMAL
 723     }
 724 
 725     private static class SignItem {
 726 
 727         private String version;
 728         private String signatureAlgorithm;
 729         // Signature algorithm that is extracted from verification output.
 730         private String extractedSignatureAlgorithm;
 731         private String tsaDigestAlgorithm;
 732         // TSA digest algorithm that is extracted from verification output.
 733         private String extractedTsaDigestAlgorithm;
 734         private CertInfo certInfo;
 735         private STATUS status;
 736         private String signedJarPath;
 737 
 738         private static SignItem build() {
 739             return new SignItem();
 740         }
 741 
 742         private SignItem version(String version) {
 743             this.version = version;
 744             return this;
 745         }
 746 
 747         private SignItem signatureAlgorithm(String signatureAlgorithm) {
 748             this.signatureAlgorithm = signatureAlgorithm;
 749             return this;
 750         }
 751 
 752         private SignItem extractedSignatureAlgorithm(
 753                 String extractedSignatureAlgorithm) {
 754             this.extractedSignatureAlgorithm = extractedSignatureAlgorithm;
 755             return this;
 756         }
 757 
 758         private SignItem tsaDigestAlgorithm(String tsaDigestAlgorithm) {
 759             this.tsaDigestAlgorithm = tsaDigestAlgorithm;
 760             return this;
 761         }
 762 
 763         private SignItem extractedTsaDigestAlgorithm(
 764                 String extractedTsaDigestAlgorithm) {
 765             this.extractedTsaDigestAlgorithm = extractedTsaDigestAlgorithm;
 766             return this;
 767         }
 768 
 769         private SignItem certInfo(CertInfo certInfo) {
 770             this.certInfo = certInfo;
 771             return this;
 772         }
 773 
 774         private SignItem status(STATUS status) {
 775             this.status = status;
 776             return this;
 777         }
 778 
 779         private SignItem signedJarPath(String signedJarPath) {
 780             this.signedJarPath = signedJarPath;
 781             return this;
 782         }
 783     }
 784 
 785     private static class VerifyItem {
 786 
 787         private String version;
 788         private STATUS status;
 789 
 790         private static VerifyItem build() {
 791             return new VerifyItem();
 792         }
 793 
 794         private VerifyItem version(String version) {
 795             this.version = version;
 796             return this;
 797         }
 798 
 799         private VerifyItem status(STATUS status) {
 800             this.status = status;
 801             return this;
 802         }
 803     }
 804 
 805     private static class ReportHeader {
 806 
 807         // Header names
 808         private static final String SIGNER_VERSION = "[Signer JDK]";
 809         private static final String SIGNATURE_ALGORITHM = "[Signature Algorithm]";
 810         private static final String TSA_DIGEST_ALGORITHM = "[TSA Digest Algorithm]";
 811         private static final String CERTIFICATE = "[Certificate]";
 812         private static final String SIGNE_STATUS = "[Status of Signing]";
 813         private static final String VERIFIER_VERSION = "[Verifier JDK]";
 814         private static final String VERIFY_STATUS = "[Status of Verifying]";
 815         private static final String FAILED = "[Failed]";
 816 
 817         // Header widths
 818         private static final int W_SIGNER_VERSION = 16;
 819         private static final int W_SIGNATURE_ALGORITHM = 23;
 820         private static final int W_TSA_DIGEST_ALGORITHM
 821                 = TSA_DIGEST_ALGORITHM.length();
 822         private static final int W_CERTIFICATE = 24;
 823         private static final int W_SIGNED = SIGNE_STATUS.length();
 824         private static final int W_VERIFIER_VERSION = W_SIGNER_VERSION;
 825         private static final int W_VERIFY_STATUS = VERIFY_STATUS.length();
 826         private static final int W_FAILED = FAILED.length();
 827 
 828         private static final String DELIMITER  = "    ";
 829         private static final String FORMAT
 830                 = "%-" + W_SIGNER_VERSION + "s" + DELIMITER
 831                 + "%-" + W_SIGNATURE_ALGORITHM + "s" + DELIMITER
 832                 + "%-" + W_TSA_DIGEST_ALGORITHM + "s" + DELIMITER
 833                 + "%-" + W_CERTIFICATE + "s" + DELIMITER
 834                 + "%-" + W_SIGNED + "s" + DELIMITER
 835                 + "%-" + W_VERIFIER_VERSION + "s" + DELIMITER
 836                 + "%-" + W_VERIFY_STATUS + "s" + DELIMITER
 837                 + "%-" + W_FAILED + "s";
 838 
 839         private static final String HEADERS = String.format(
 840                 ReportHeader.FORMAT,
 841                 SIGNER_VERSION,
 842                 SIGNATURE_ALGORITHM,
 843                 TSA_DIGEST_ALGORITHM,
 844                 CERTIFICATE,
 845                 SIGNE_STATUS,
 846                 VERIFIER_VERSION,
 847                 VERIFY_STATUS,
 848                 FAILED);
 849 
 850     }
 851 
 852     private static class ReportItem {
 853 
 854         private final SignItem signItem;
 855         private final VerifyItem verifyItem;
 856 
 857         private ReportItem(SignItem signItem, VerifyItem verifyItem) {
 858             this.signItem = signItem;
 859             this.verifyItem = verifyItem;
 860         }
 861 
 862         @Override
 863         public String toString() {
 864             return String.format(ReportHeader.FORMAT,
 865                     signItem.version,
 866                     null2Default(signItem.signatureAlgorithm,
 867                             signItem.extractedSignatureAlgorithm),
 868                     null2Default(signItem.tsaDigestAlgorithm,
 869                             signItem.extractedTsaDigestAlgorithm),
 870                     signItem.certInfo,
 871                     signItem.status,
 872                     verifyItem.version,
 873                     verifyItem.status,
 874                     verifyItem.status == STATUS.ERROR ? "X" : "");
 875         }
 876 
 877         // If a value is null, then displays the default value or N/A.
 878         private static String null2Default(String value, String defaultValue) {
 879             return value == null 
 880                     ? DEFAULT + "(" + (defaultValue == null 
 881                                        ? "N/A"
 882                                        : defaultValue) + ")"
 883                     : value;
 884         }
 885     }
 886 }