/* * 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 JDK build doesn't support some specific algorithms. * * 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 signature algorithm and timestamp * digest algorithm cannot be extracted from verification output. And this * JDK should support as many as possible signature algorithms. So, the latest * JDK build is recommended. * * Usage: jtreg [-options] \ * [-Dstrict= \ * -DproxyHost= -DproxyPort= \ * -Dtsa= \ * -DjdkListFile= \ * -DjdkList= \ * -DjavaSecurityFile=] \ * /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 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 by jtreg * option -jdk will be used as the only one JDK in the JDK list. * * 7. javaSecurityFile= * This property indicates an alternative java security properties * file. The default value is the path of file java.scurity that is * distributed with this test. * * 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. Certificate: Certificate identifier. * The identifier consists of specific attributes of the certificate. * The naming convention is: * KeyAlgorithm_DigestAlgorithm_KeySize[_Expired] * 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 letter 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 * @compile -source 1.6 -target 1.6 JdkUtils.java * @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.List; import java.util.Map; 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_JAR_NAME = "test.jar"; private static final String TEST_SRC = System.getProperty("test.src"); private static final String TEST_CLASSES = System.getProperty("test.classes"); 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. // The test provides a default one, which only contains two lines: // jdk.certpath.disabledAlgorithms=MD2, MD5 // jdk.jar.disabledAlgorithms=MD2, MD5 private static final String JAVA_SECURITY = System.getProperty( "javaSecurityFile", TEST_SRC + "/java.security"); private static final String PASSWORD = "testpass"; private static final String KEYSTORE = "testKeystore"; private static final String DEFAULT = "DEFAULT"; private static final String RSA = "RSA"; private static final String DSA = "DSA"; private static final String EC = "EC"; private static final String[] KEY_ALGORITHMS = new String[] { RSA, DSA, EC}; private static final String SHA1 = "SHA-1"; private static final String SHA256 = "SHA-256"; private static final String SHA512 = "SHA-512"; private static final String[] DIGEST_ALGORITHMS = new String[] { SHA1, SHA256, SHA512, DEFAULT}; private static final boolean[] EXPIRED = new boolean[] { false, true}; 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(); List reportItems = test(jdkList(), createCertificates()); 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."); } } // Creates a jar file that contains an empty file. private static void createJar() throws IOException { String testFile = "test"; new File(testFile).createNewFile(); JarUtils.createJar(TEST_JAR_NAME, testFile); } // Creates a key store that includes a set of (expired) certificates with // various algorithms. private static Map createCertificates() throws Throwable { Map certs = new HashMap(); for(String keyAlgorithm : KEY_ALGORITHMS) { for(String digestAlgorithm : DIGEST_ALGORITHMS) { if (DEFAULT.equals(digestAlgorithm)) { continue; } for(int keySize : keySizes(keyAlgorithm)) { for(boolean expired : EXPIRED) { CertInfo certInfo = new CertInfo( keyAlgorithm, digestAlgorithm, keySize, expired); String alias = createCertificate(certInfo); certs.put(certInfo, alias); } } } } return certs; } // Creates/Updates a key store that adds a certificate with specific algorithm. private static String createCertificate(CertInfo certInfo) throws Throwable { String alias = certInfo.toString(); OutputAnalyzer outputAnalyzer = execTool( TEST_JDK + "/bin/keytool", "-J-Djava.security.properties=" + JAVA_SECURITY, "-v", "-storetype", "jks", "-genkey", "-keyalg", certInfo.keyAlgorithm, "-sigalg", sigalg(certInfo.digestAlgorithm, certInfo.keyAlgorithm), "-keysize", certInfo.keySize + "", "-dname", "CN=" + alias, "-alias", alias, "-keypass", PASSWORD, "-storepass", PASSWORD, "-startdate", "-2d", "-validity", certInfo.expired ? "1" : "3650", "-keystore", KEYSTORE); if (outputAnalyzer.getExitValue() == 0 && !outputAnalyzer.getOutput().matches("[Ee]xception")) { return alias; } else { return null; } } private static String sigalg(String digestAlgorithm, String keyAlgorithm) { if (digestAlgorithm == DEFAULT) { return null; } String keyName = EC.equals(keyAlgorithm) ? "ECDSA" : keyAlgorithm; return digestAlgorithm.replace("-", "") + "with" + keyName; } // 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[jdkPaths.size()]); } String jdkList = System.getProperty("jdkList", TEST_JDK); System.out.println("JDK List:\n" + jdkList); String[] jdkPaths = jdkList.split(","); return jdkPaths; } // A JDK (signer) signs a jar with a variety of algorithms, and then all of // JDKs (verifiers), including the signer itself, try to verify the signed // jars respectively. private static List test(String[] jdkPaths, Map certs) throws Throwable { List reportItems = new ArrayList(); for (String signerPath : jdkPaths) { JdkInfo signerInfo = JdkInfoMap.getJdkInfo(signerPath); for (String keyAlgorithm : KEY_ALGORITHMS) { for (String digestAlgorithm : DIGEST_ALGORITHMS) { String sigalg = sigalg(digestAlgorithm, keyAlgorithm); // If the signature algorithm is not supported by the JDK, // it cannot try to sign jar with this algorithm. if (sigalg != null && !signerInfo.isSupportedSigalg(sigalg)) { continue; } // If the JDK doesn't support option -tsadigestalg, the // associated cases just be ignored. if (digestAlgorithm != DEFAULT && !signerInfo.supportsTsadigestalg) { continue; } for (int keySize : keySizes(keyAlgorithm)) { for (boolean expired : EXPIRED) { String certDigest = digestAlgorithm; int certKeySize = keySize; // If the digest algorithm is not specified, then it // uses certificate with SHA256 digest and 1024 key // size. if (digestAlgorithm == DEFAULT) { certDigest = SHA256; certKeySize = 1024; } CertInfo certInfo = new CertInfo( keyAlgorithm, certDigest, certKeySize, expired); String alias = certs.get(certInfo); if (alias == null) { continue; } String signedJarPath = signerInfo.version + "_" + certInfo + ".jar"; String tsadigestalg = digestAlgorithm != DEFAULT ? digestAlgorithm : null; String signOutput = signJar( signerInfo.jarsignerPath, sigalg, tsadigestalg, alias, signedJarPath); STATUS signingStatus = signingStatus(signOutput); // If signing fails, the following verifying has to // be ignored. if (signingStatus == STATUS.ERROR) { continue; } SignItem signItem = SignItem.build() .version(signerInfo.version) .signatureAlgorithm(sigalg) .tsaDigestAlgorithm(tsadigestalg) .certInfo(certInfo) .status(signingStatus) .signedJarPath(signedJarPath); // Using the testing JDK, which is specified by jtreg // option "-jdk", to verify the signed jar and extract // the signature algorithm and timestamp digest algorithm. String output = verifyJar(TEST_JARSIGNER, signedJarPath); signItem.extractedSignatureAlgorithm(extract(output, " *Signature algorithm.*", ".*: |,.*")); signItem.extractedTsaDigestAlgorithm(extract(output, " *Timestamp digest algorithm.*", ".*: ")); reportItems.addAll( verifyJarByJDKs(jdkPaths, signItem)); } } } } } return reportItems; } private static List verifyJarByJDKs(String[] jdkPaths, SignItem signItem) throws Throwable { List reportItems = new ArrayList(); for (String verifierPath : jdkPaths) { JdkInfo verifierInfo = JdkInfoMap.getJdkInfo(verifierPath); // If strict is false and the verifier JDK doesn't support this // signature algorithm, the case just be ignored. if (!STRICT && signItem.signatureAlgorithm != null && !verifierInfo.isSupportedSigalg( signItem.signatureAlgorithm)) { continue; } VerifyItem verifyItem = verifyJarByJDK(verifierInfo, signItem); ReportItem reportItem = new ReportItem(signItem, verifyItem); reportItems.add(reportItem); System.out.println("Result:\n" + reportItem); } return reportItems; } private static VerifyItem verifyJarByJDK(JdkInfo verifierInfo, SignItem signItem) throws Throwable { String verifyOutput = verifyJar(verifierInfo.jarsignerPath, signItem.signedJarPath); STATUS verifyingStatus = verifyingStatus(verifyOutput); // If strict is true, it has to check if the default timestamp digest // algorithm is SHA-256. if (verifyingStatus != STATUS.ERROR) { verifyingStatus = (STRICT && signItem.tsaDigestAlgorithm == DEFAULT && signItem.extractedTsaDigestAlgorithm != null && !signItem.extractedTsaDigestAlgorithm .matches("SHA-?256")) ? STATUS.ERROR : verifyingStatus; } VerifyItem verifyItem = VerifyItem.build() .version(verifierInfo.version) .status(verifyingStatus); return verifyItem; } // Return key sizes according to the specified key algorithm. private static int[] keySizes(String keyAlgorithm) { if (keyAlgorithm == RSA || keyAlgorithm == DSA) { return new int[] { 1024, 2048 }; } else if (keyAlgorithm == EC) { return new int[] { 384, 571 }; } return null; } // Determines the status of signing. private static STATUS signingStatus(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 verifyingStatus(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; } } // 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 = execTool( 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 = execTool( 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 the specified function on JdkUtils by the specified JDK. private static String execJdkUtils(String jdkPath, String method, String... args) throws Throwable { String[] cmd = new String[args.length + 5]; cmd[0] = jdkPath + "/bin/java"; cmd[1] = "-cp"; cmd[2] = TEST_CLASSES; cmd[3] = JdkUtils.class.getName(); cmd[4] = method; System.arraycopy(args, 0, cmd, 5, args.length); return ProcessTools.executeCommand(cmd).getOutput(); } // Executes the specified JDK tools, such as keytool and jarsigner, and // ensures the output is in US English. private static OutputAnalyzer execTool(String toolPath, String... args) throws Throwable { String[] cmd = new String[args.length + 4]; cmd[0] = toolPath; cmd[1] = "-J-Duser.language=en"; cmd[2] = "-J-Duser.country=US"; cmd[3] = "-J-Djava.security.egd=file:/dev/./urandom"; System.arraycopy(args, 0, cmd, 4, args.length); return ProcessTools.executeCommand(cmd); } private static class JdkInfoMap { private static Map map = new HashMap(); private static JdkInfo getJdkInfo(String jdkPath) throws Throwable { if (!map.containsKey(jdkPath)) { map.put(jdkPath, new JdkInfo(jdkPath)); } return map.get(jdkPath); } } private static class JdkInfo { private final String jdkPath; private final String jarsignerPath; private final String version; private final boolean supportsTsadigestalg; private Map sigalgMap = new HashMap(); private JdkInfo(String jdkPath) throws Throwable { this.jdkPath = jdkPath; version = execJdkUtils(jdkPath, JdkUtils.M_JAVA_RUNTIME_VERSION); if (version == null || version.trim().isEmpty()) { throw new RuntimeException( "Cannot determine the JDK version: " + jdkPath); } jarsignerPath = jarsignerPath(jdkPath); supportsTsadigestalg = execTool(jarsignerPath, "-help") .getOutput().contains("-tsadigestalg"); } private boolean isSupportedSigalg(String sigalg) throws Throwable { if (!sigalgMap.containsKey(sigalg)) { boolean isSupported = "true".equalsIgnoreCase( execJdkUtils( jdkPath, JdkUtils.M_IS_SUPPORTED_SIGALG, sigalg)); sigalgMap.put(sigalg, isSupported); } return sigalgMap.get(sigalg); } } private static class CertInfo { private final String keyAlgorithm; private final String digestAlgorithm; private final int keySize; private final boolean expired; private CertInfo(String keyAlgorithm, String digestAlgorithm, int keySize, boolean expired) { this.keyAlgorithm = keyAlgorithm; this.digestAlgorithm = digestAlgorithm; this.keySize = keySize; this.expired = expired; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (expired ? 1231 : 1237); result = prime * result + ((keyAlgorithm == null) ? 0 : keyAlgorithm.hashCode()); result = prime * result + keySize; result = prime * result + ((digestAlgorithm == null) ? 0 : digestAlgorithm.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; CertInfo other = (CertInfo) obj; if (expired != other.expired) return false; if (keyAlgorithm == null) { if (other.keyAlgorithm != null) return false; } else if (!keyAlgorithm.equals(other.keyAlgorithm)) return false; if (keySize != other.keySize) return false; if (digestAlgorithm == null) { if (other.digestAlgorithm != null) return false; } else if (!digestAlgorithm.equals(other.digestAlgorithm)) return false; return true; } @Override public String toString() { return keyAlgorithm + "_" + digestAlgorithm + "_" + keySize + (expired ? "_Expired" : ""); } } private static enum STATUS { // jar is signed/verified 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; // Signature algorithm that is extracted from verification output. private String extractedSignatureAlgorithm; private String tsaDigestAlgorithm; // TSA digest algorithm that is extracted from verification output. private String extractedTsaDigestAlgorithm; private CertInfo certInfo; 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 extractedSignatureAlgorithm( String extractedSignatureAlgorithm) { this.extractedSignatureAlgorithm = extractedSignatureAlgorithm; return this; } private SignItem tsaDigestAlgorithm(String tsaDigestAlgorithm) { this.tsaDigestAlgorithm = tsaDigestAlgorithm; return this; } private SignItem extractedTsaDigestAlgorithm( String extractedTsaDigestAlgorithm) { this.extractedTsaDigestAlgorithm = extractedTsaDigestAlgorithm; return this; } private SignItem certInfo(CertInfo certInfo) { this.certInfo = certInfo; return this; } private SignItem status(STATUS status) { this.status = status; return this; } private SignItem signedJarPath(String signedJarPath) { this.signedJarPath = signedJarPath; return this; } } 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 CERTIFICATE = "[Certificate]"; 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_CERTIFICATE = 24; 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_CERTIFICATE + "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, CERTIFICATE, 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, null2Default(signItem.signatureAlgorithm, signItem.extractedSignatureAlgorithm), null2Default(signItem.tsaDigestAlgorithm, signItem.extractedTsaDigestAlgorithm), signItem.certInfo, 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; } } }