jdk-24/test/jdk/sun/security/tools/jarsigner/compatibility/Compatibility.java
2022-03-24 22:50:26 +00:00

1621 lines
64 KiB
Java

/*
* Copyright (c) 2017, 2022, 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 8217375 8260286 8267319
* @summary This test is used to verify the compatibility of jarsigner across
* different JDK releases. It also can be used to check jar signing (w/
* and w/o TSA) and to verify some specific signing and digest algorithms.
* Note that this is a manual test. For more details about the test and
* its usages, please look through the README.
*
* @library /test/lib ../warnings
* @compile -source 1.7 -target 1.7 JdkUtils.java
* @run main/manual/othervm Compatibility
*/
import static java.nio.charset.StandardCharsets.UTF_8;
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.OutputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.jar.Attributes.Name;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
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_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 JdkInfo TEST_JDK_INFO;
private static final String PROXY_HOST = System.getProperty("proxyHost");
private static final String PROXY_PORT = System.getProperty("proxyPort", "80");
// 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.jks";
private static final String RSA = "RSA";
private static final String DSA = "DSA";
private static final String EC = "EC";
private static String[] KEY_ALGORITHMS;
private static final String[] DEFAULT_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 SHA384 = "SHA-384";
private static final String SHA512 = "SHA-512";
private static final String DEFAULT = "DEFAULT";
private static String[] DIGEST_ALGORITHMS;
private static final String[] DEFAULT_DIGEST_ALGORITHMS = new String[] {
SHA1,
SHA256,
SHA384,
SHA512, // note: digests break onto continuation line in manifest
DEFAULT};
private static final boolean[] EXPIRED =
Boolean.valueOf(System.getProperty("expired", "true")) ?
new boolean[] { false, true } : new boolean[] { false };
private static final boolean TEST_COMPREHENSIVE_JAR_CONTENTS =
Boolean.valueOf(System.getProperty(
"testComprehensiveJarContents", "false"));
private static final boolean TEST_JAR_UPDATE =
Boolean.valueOf(System.getProperty("testJarUpdate", "false"));
private static final boolean STRICT =
Boolean.valueOf(System.getProperty("strict", "false"));
private static final Calendar CALENDAR = Calendar.getInstance();
private static final DateFormat DATE_FORMAT
= new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
// The certificate validity period in minutes. The default value is 1440
// minutes, namely 1 day.
private static final int CERT_VALIDITY
= Integer.valueOf(System.getProperty("certValidity", "1440"));
static {
if (CERT_VALIDITY < 1 || CERT_VALIDITY > 1440) {
throw new RuntimeException(
"certValidity out of range [1, 1440]: " + CERT_VALIDITY);
}
}
// If true, an additional verifying will be triggered after all of
// valid certificates expire. The default value is false.
public static final boolean DELAY_VERIFY
= Boolean.valueOf(System.getProperty("delayVerify", "false"));
private static long lastCertStartTime;
private static DetailsOutputStream detailsOutput;
private static int sigfileCounter;
private static String nextSigfileName(String alias, String u, String s) {
String sigfileName = "" + (++sigfileCounter);
System.out.println("using sigfile " + sigfileName + " for alias "
+ alias + " signing " + u + ".jar to " + s + ".jar");
return sigfileName;
}
public static void main(String... args) throws Throwable {
// Backups stdout and stderr.
PrintStream origStdOut = System.out;
PrintStream origStdErr = System.err;
detailsOutput = new DetailsOutputStream(outfile());
// Redirects the system output to a custom one.
PrintStream printStream = new PrintStream(detailsOutput);
System.setOut(printStream);
System.setErr(printStream);
TEST_JDK_INFO = new JdkInfo(TEST_JDK);
List<TsaInfo> tsaList = tsaInfoList();
List<JdkInfo> jdkInfoList = jdkInfoList();
List<CertInfo> certList = createCertificates(jdkInfoList);
List<SignItem> signItems =
test(jdkInfoList, tsaList, certList, createJars());
boolean failed = generateReport(jdkInfoList, tsaList, signItems);
// Restores the original stdout and stderr.
System.setOut(origStdOut);
System.setErr(origStdErr);
if (failed) {
throw new RuntimeException("At least one test case failed. "
+ "Please check the failed row(s) in report.html "
+ "or failedReport.html.");
}
}
private static SignItem createJarFile(String jar, Manifest m,
String... files) throws IOException {
JarUtils.createJarFile(Path.of(jar), m, Path.of("."),
Arrays.stream(files).map(Path::of).toArray(Path[]::new));
return SignItem.build()
.signedJar(jar.replaceAll("[.]jar$", ""))
.addContentFiles(Arrays.stream(files).collect(Collectors.toList()));
}
private static String createDummyFile(String name) throws IOException {
if (name.contains("/")) new File(name).getParentFile().mkdir();
try (OutputStream fos = new FileOutputStream(name)) {
fos.write(name.getBytes(UTF_8));
}
return name;
}
// Creates one or more jar files to test
private static List<SignItem> createJars() throws IOException {
List<SignItem> jarList = new ArrayList<>();
Manifest m = new Manifest();
m.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0");
// creates a jar file that contains a dummy file
jarList.add(createJarFile("test.jar", m, createDummyFile("dummy")));
if (TEST_COMPREHENSIVE_JAR_CONTENTS) {
// empty jar file so that jarsigner will add a default manifest
jarList.add(createJarFile("empty.jar", m));
// jar file that contains only an empty manifest with empty main
// attributes (due to missing "Manifest-Version" header)
JarUtils.createJar("nomainatts.jar");
jarList.add(SignItem.build().signedJar("nomainatts"));
// creates a jar file that contains several files.
jarList.add(createJarFile("files.jar", m,
IntStream.range(1, 9).boxed().map(i -> {
try {
return createDummyFile("dummy" + i);
} catch (IOException e) {
throw new RuntimeException(e);
}
}).toArray(String[]::new)
));
// forces a line break by exceeding the line width limit of 72 bytes
// in the filename and hence manifest entry name
jarList.add(createJarFile("longfilename.jar", m,
createDummyFile("test".repeat(20))));
// another interesting case is with different digest algorithms
// resulting in digests broken across line breaks onto continuation
// lines. these however are set with the 'digestAlgs' option or
// include all digest algorithms by default, see SignTwice.java.
}
return jarList;
}
// updates a signed jar file by adding another file
private static List<SignItem> updateJar(SignItem prev) throws IOException {
List<SignItem> jarList = new ArrayList<>();
// sign unmodified jar again
Files.copy(Path.of(prev.signedJar + ".jar"),
Path.of(prev.signedJar + "-signagainunmodified.jar"));
jarList.add(SignItem.build(prev)
.signedJar(prev.signedJar + "-signagainunmodified"));
String oldJar = prev.signedJar;
String newJar = oldJar + "-addfile";
String triggerUpdateFile = "addfile";
JarUtils.updateJar(oldJar + ".jar", newJar + ".jar", triggerUpdateFile);
jarList.add(SignItem.build(prev).signedJar(newJar)
.addContentFiles(Arrays.asList(triggerUpdateFile)));
return jarList;
}
// Creates a key store that includes a set of valid/expired certificates
// with various algorithms.
private static List<CertInfo> createCertificates(List<JdkInfo> jdkInfoList)
throws Throwable {
List<CertInfo> certList = new ArrayList<>();
Set<String> expiredCertFilter = new HashSet<>();
for (JdkInfo jdkInfo : jdkInfoList) {
for (String keyAlgorithm : keyAlgs()) {
if (!jdkInfo.supportsKeyAlg(keyAlgorithm)) continue;
for (int keySize : keySizes(keyAlgorithm)) {
for (String digestAlgorithm : digestAlgs()) {
for(boolean expired : EXPIRED) {
// It creates only one expired certificate for one
// key algorithm.
if (expired
&& !expiredCertFilter.add(keyAlgorithm)) {
continue;
}
CertInfo certInfo = new CertInfo(
jdkInfo,
keyAlgorithm,
digestAlgorithm,
keySize,
expired);
// If the signature algorithm is not supported by the
// JDK, it cannot try to sign jar with this algorithm.
String sigalg = certInfo.sigalg();
if (sigalg != null &&
!jdkInfo.isSupportedSigalg(sigalg)) {
continue;
}
createCertificate(jdkInfo, certInfo);
certList.add(certInfo);
}
}
}
}
}
System.out.println("the keystore contents:");
for (JdkInfo jdkInfo : jdkInfoList) {
execTool(jdkInfo.jdkPath + "/bin/keytool", new String[] {
"-v",
"-storetype",
"jks",
"-storepass",
PASSWORD,
"-keystore",
KEYSTORE,
"-list"
});
}
return certList;
}
// Creates/Updates a key store that adds a certificate with specific algorithm.
private static void createCertificate(JdkInfo jdkInfo, CertInfo certInfo)
throws Throwable {
List<String> arguments = new ArrayList<>();
arguments.add("-J-Djava.security.properties=" + JAVA_SECURITY);
arguments.add("-v");
arguments.add("-debug");
arguments.add("-storetype");
arguments.add("jks");
arguments.add("-keystore");
arguments.add(KEYSTORE);
arguments.add("-storepass");
arguments.add(PASSWORD);
arguments.add(jdkInfo.majorVersion < 6 ? "-genkey" : "-genkeypair");
arguments.add("-keyalg");
arguments.add(certInfo.keyAlgorithm);
String sigalg = certInfo.sigalg();
if (sigalg != null) {
arguments.add("-sigalg");
arguments.add(sigalg);
}
if (certInfo.keySize != 0) {
arguments.add("-keysize");
arguments.add(certInfo.keySize + "");
}
arguments.add("-dname");
arguments.add("CN=" + certInfo);
arguments.add("-alias");
arguments.add(certInfo.alias());
arguments.add("-keypass");
arguments.add(PASSWORD);
arguments.add("-startdate");
arguments.add(startDate(certInfo.expired));
arguments.add("-validity");
// arguments.add(DELAY_VERIFY ? "1" : "222"); // > six months no warn
arguments.add("1");
OutputAnalyzer outputAnalyzer = execTool(
jdkInfo.jdkPath + "/bin/keytool",
arguments.toArray(new String[arguments.size()]));
if (outputAnalyzer.getExitValue() != 0
|| outputAnalyzer.getOutput().matches("[Ee]xception")
|| outputAnalyzer.getOutput().matches(Test.ERROR + " ?")) {
System.out.println(outputAnalyzer.getOutput());
throw new Exception("error generating a key pair: " + arguments);
}
}
// The validity period of a certificate always be 1 day. For creating an
// expired certificate, the start date is the time before 1 day, then the
// certificate expires immediately. And for creating a valid certificate,
// the start date is the time before (1 day - CERT_VALIDITY minutes), then
// the certificate will expires in CERT_VALIDITY minutes.
private static String startDate(boolean expiredCert) {
CALENDAR.setTime(new Date());
if (DELAY_VERIFY || expiredCert) {
// corresponds to '-validity 1'
CALENDAR.add(Calendar.DAY_OF_MONTH, -1);
}
if (DELAY_VERIFY && !expiredCert) {
CALENDAR.add(Calendar.MINUTE, CERT_VALIDITY);
}
Date startDate = CALENDAR.getTime();
if (!expiredCert) {
lastCertStartTime = startDate.getTime();
}
return DATE_FORMAT.format(startDate);
}
private static String outfile() {
return System.getProperty("o");
}
// Retrieves JDK info from the file which is specified by property
// jdkListFile, or from property jdkList if jdkListFile is not available.
private static List<JdkInfo> jdkInfoList() throws Throwable {
String[] jdkList = list("jdkList");
if (jdkList.length == 0) {
jdkList = new String[] { "TEST_JDK" };
}
List<JdkInfo> jdkInfoList = new ArrayList<>();
int index = 0;
for (String jdkPath : jdkList) {
JdkInfo jdkInfo = "TEST_JDK".equalsIgnoreCase(jdkPath) ?
TEST_JDK_INFO : new JdkInfo(jdkPath);
// The JDK version must be unique.
if (!jdkInfoList.contains(jdkInfo)) {
jdkInfo.index = index++;
jdkInfo.version = String.format(
"%s(%d)", jdkInfo.version, jdkInfo.index);
jdkInfoList.add(jdkInfo);
} else {
System.out.println("The JDK version is duplicate: " + jdkPath);
}
}
return jdkInfoList;
}
private static List<String> keyAlgs() throws IOException {
if (KEY_ALGORITHMS == null) KEY_ALGORITHMS = list("keyAlgs");
if (KEY_ALGORITHMS.length == 0)
return Arrays.asList(DEFAULT_KEY_ALGORITHMS);
return Arrays.stream(KEY_ALGORITHMS).map(a -> a.split(";")[0])
.collect(Collectors.toList());
}
// Return key sizes according to the specified key algorithm.
private static int[] keySizes(String keyAlgorithm) throws IOException {
if (KEY_ALGORITHMS == null) KEY_ALGORITHMS = list("keyAlgs");
for (String keyAlg : KEY_ALGORITHMS) {
String[] split = (keyAlg + " ").split(";");
if (keyAlgorithm.equals(split[0].trim()) && split.length > 1) {
int sizes[] = new int[split.length - 1];
for (int i = 1; i <= sizes.length; i++)
sizes[i - 1] = split[i].isBlank() ? 0 : // default
Integer.parseInt(split[i].trim());
return sizes;
}
}
// defaults
if (RSA.equals(keyAlgorithm) || DSA.equals(keyAlgorithm)) {
return new int[] { 1024, 2048, 0 }; // 0 is no keysize specified
} else if (EC.equals(keyAlgorithm)) {
return new int[] { 384, 571, 0 }; // 0 is no keysize specified
} else {
throw new RuntimeException("problem determining key sizes");
}
}
private static List<String> digestAlgs() throws IOException {
if (DIGEST_ALGORITHMS == null) DIGEST_ALGORITHMS = list("digestAlgs");
if (DIGEST_ALGORITHMS.length == 0)
return Arrays.asList(DEFAULT_DIGEST_ALGORITHMS);
return Arrays.asList(DIGEST_ALGORITHMS);
}
// Retrieves TSA info from the file which is specified by property tsaListFile,
// or from property tsaList if tsaListFile is not available.
private static List<TsaInfo> tsaInfoList() throws IOException {
String[] tsaList = list("tsaList");
List<TsaInfo> tsaInfoList = new ArrayList<>();
for (int i = 0; i < tsaList.length; i++) {
String[] values = tsaList[i].split(";digests=");
String[] digests = new String[0];
if (values.length == 2) {
digests = values[1].split(",");
}
String tsaUrl = values[0];
if (tsaUrl.isEmpty() || tsaUrl.equalsIgnoreCase("notsa")) {
tsaUrl = null;
}
TsaInfo bufTsa = new TsaInfo(i, tsaUrl);
for (String digest : digests) {
bufTsa.addDigest(digest.toUpperCase());
}
tsaInfoList.add(bufTsa);
}
if (tsaInfoList.size() == 0) {
throw new RuntimeException("TSA service is mandatory unless "
+ "'notsa' specified explicitly.");
}
return tsaInfoList;
}
private static String[] list(String listProp) throws IOException {
String listFileProp = listProp + "File";
String listFile = System.getProperty(listFileProp);
if (!isEmpty(listFile)) {
System.out.println(listFileProp + "=" + listFile);
List<String> list = new ArrayList<>();
BufferedReader reader = new BufferedReader(
new FileReader(listFile));
String line;
while ((line = reader.readLine()) != null) {
String item = line.trim();
if (!item.isEmpty()) {
list.add(item);
}
}
reader.close();
return list.toArray(new String[list.size()]);
}
String list = System.getProperty(listProp);
System.out.println(listProp + "=" + list);
return !isEmpty(list) ? list.split("#") : new String[0];
}
private static boolean isEmpty(String str) {
return str == null || str.isEmpty();
}
// 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<SignItem> test(List<JdkInfo> jdkInfoList,
List<TsaInfo> tsaInfoList, List<CertInfo> certList,
List<SignItem> jars) throws Throwable {
detailsOutput.transferPhase();
List<SignItem> signItems = new ArrayList<>();
signItems.addAll(signing(jdkInfoList, tsaInfoList, certList, jars));
if (TEST_JAR_UPDATE) {
signItems.addAll(signing(jdkInfoList, tsaInfoList, certList,
updating(signItems.stream().filter(
x -> x.status != Status.ERROR)
.collect(Collectors.toList()))));
}
detailsOutput.transferPhase();
for (SignItem signItem : signItems) {
for (JdkInfo verifierInfo : jdkInfoList) {
if (!verifierInfo.supportsKeyAlg(
signItem.certInfo.keyAlgorithm)) continue;
VerifyItem verifyItem = VerifyItem.build(verifierInfo);
verifyItem.addSignerCertInfos(signItem);
signItem.addVerifyItem(verifyItem);
verifying(signItem, verifyItem);
}
}
// if lastCertExpirationTime passed already now, probably some
// certificate was already expired during jar signature verification
// (jarsigner -verify) and the test should probably be repeated with an
// increased validity period -DcertValidity CERT_VALIDITY
long lastCertExpirationTime = lastCertStartTime + 24 * 60 * 60 * 1000;
if (lastCertExpirationTime < System.currentTimeMillis()) {
throw new AssertionError("CERT_VALIDITY (" + CERT_VALIDITY
+ " [minutes]) was too short. "
+ "Creating and signing the jars took longer, "
+ "presumably at least "
+ ((lastCertExpirationTime - System.currentTimeMillis())
/ 60 * 1000 + CERT_VALIDITY) + " [minutes].");
}
if (DELAY_VERIFY) {
detailsOutput.transferPhase();
System.out.print("Waiting for delay verifying");
while (System.currentTimeMillis() < lastCertExpirationTime) {
TimeUnit.SECONDS.sleep(30);
System.out.print(".");
}
System.out.println();
System.out.println("Delay verifying starts");
for (SignItem signItem : signItems) {
for (VerifyItem verifyItem : signItem.verifyItems) {
verifying(signItem, verifyItem);
}
}
}
detailsOutput.transferPhase();
return signItems;
}
private static List<SignItem> signing(List<JdkInfo> jdkInfos,
List<TsaInfo> tsaList, List<CertInfo> certList,
List<SignItem> unsignedJars) throws Throwable {
List<SignItem> signItems = new ArrayList<>();
for (CertInfo certInfo : certList) {
JdkInfo signerInfo = certInfo.jdkInfo;
String keyAlgorithm = certInfo.keyAlgorithm;
String sigDigestAlgorithm = certInfo.digestAlgorithm;
int keySize = certInfo.keySize;
boolean expired = certInfo.expired;
for (String jarDigestAlgorithm : digestAlgs()) {
if (DEFAULT.equals(jarDigestAlgorithm)) {
jarDigestAlgorithm = null;
}
for (TsaInfo tsaInfo : tsaList) {
String tsaUrl = tsaInfo.tsaUrl;
List<String> tsaDigestAlgs = digestAlgs();
// no point in specifying a tsa digest algorithm
// for no TSA, except maybe it would issue a warning.
if (tsaUrl == null) tsaDigestAlgs = Arrays.asList(DEFAULT);
// If the JDK doesn't support option -tsadigestalg, the
// associated cases can just be ignored.
if (!signerInfo.supportsTsadigestalg) {
tsaDigestAlgs = Arrays.asList(DEFAULT);
}
for (String tsaDigestAlg : tsaDigestAlgs) {
if (DEFAULT.equals(tsaDigestAlg)) {
tsaDigestAlg = null;
} else if (!tsaInfo.isDigestSupported(tsaDigestAlg)) {
// It has to ignore the digest algorithm, which
// is not supported by the TSA server.
continue;
}
if (tsaUrl != null && TsaFilter.filter(
signerInfo.version,
tsaDigestAlg,
expired,
tsaInfo.index)) {
continue;
}
for (SignItem prevSign : unsignedJars) {
String unsignedJar = prevSign.signedJar;
SignItem signItem = SignItem.build(prevSign)
.certInfo(certInfo)
.jdkInfo(signerInfo);
String signedJar = unsignedJar + "-" + "JDK_" + (
signerInfo.version + "-CERT_" + certInfo).
replaceAll("[^a-z_0-9A-Z.]+", "-");
if (jarDigestAlgorithm != null) {
signedJar += "-DIGESTALG_" + jarDigestAlgorithm;
signItem.digestAlgorithm(jarDigestAlgorithm);
}
if (tsaUrl == null) {
signItem.tsaIndex(-1);
} else {
signedJar += "-TSA_" + tsaInfo.index;
signItem.tsaIndex(tsaInfo.index);
if (tsaDigestAlg != null) {
signedJar += "-TSADIGALG_" + tsaDigestAlg;
signItem.tsaDigestAlgorithm(tsaDigestAlg);
}
}
signItem.signedJar(signedJar);
String signingId = signingId(signItem);
detailsOutput.writeAnchorName(signingId,
"Signing: " + signingId);
OutputAnalyzer signOA = signJar(
signerInfo.jarsignerPath,
certInfo.sigalg(),
jarDigestAlgorithm,
tsaDigestAlg,
tsaUrl,
certInfo.alias(),
unsignedJar,
signedJar);
Status signingStatus = signingStatus(signOA,
tsaUrl != null);
signItem.status(signingStatus);
signItems.add(signItem);
}
}
}
}
}
return signItems;
}
private static List<SignItem> updating(List<SignItem> prevSignItems)
throws IOException {
List<SignItem> updateItems = new ArrayList<>();
for (SignItem prevSign : prevSignItems) {
updateItems.addAll(updateJar(prevSign));
}
return updateItems;
}
private static void verifying(SignItem signItem, VerifyItem verifyItem)
throws Throwable {
// TODO: how will be ensured that the first verification is not after valid period expired which is only one minute?
boolean delayVerify = verifyItem.status != Status.NONE;
String verifyingId = verifyingId(signItem, verifyItem, delayVerify);
detailsOutput.writeAnchorName(verifyingId, "Verifying: " + verifyingId);
OutputAnalyzer verifyOA = verifyJar(verifyItem.jdkInfo.jarsignerPath,
signItem.signedJar, verifyItem.certInfo == null ? null :
verifyItem.certInfo.alias());
Status verifyingStatus = verifyingStatus(signItem, verifyItem, verifyOA);
try {
String match = "^ ("
+ " Signature algorithm: " + signItem.certInfo.
expectedSigalg() + ", " + signItem.certInfo.
expectedKeySize() + "-bit key"
+ ")|("
+ " Digest algorithm: " + signItem.expectedDigestAlg()
+ (isWeakAlg(signItem.expectedDigestAlg()) ? " \\(weak\\)" : "")
+ (signItem.tsaIndex < 0 ? "" :
")|("
+ "Timestamped by \".+\" on .*"
+ ")|("
+ " Timestamp digest algorithm: "
+ signItem.expectedTsaDigestAlg()
+ ")|("
+ " Timestamp signature algorithm: .*"
)
+ ")$";
verifyOA.stdoutShouldMatchByLine(
"^- Signed by \"CN=" + signItem.certInfo.toString()
.replaceAll("[.]", "[.]") + "\"$",
"^(- Signed by \"CN=.+\")?$",
match);
} catch (Throwable e) {
e.printStackTrace();
verifyingStatus = Status.ERROR;
}
if (!delayVerify) {
verifyItem.status(verifyingStatus);
} else {
verifyItem.delayStatus(verifyingStatus);
}
if (verifyItem.prevVerify != null) {
verifying(signItem, verifyItem.prevVerify);
}
}
// Determines the status of signing.
private static Status signingStatus(OutputAnalyzer outputAnalyzer,
boolean tsa) {
if (outputAnalyzer.getExitValue() != 0) {
return Status.ERROR;
}
if (!outputAnalyzer.getOutput().contains(Test.JAR_SIGNED)) {
return Status.ERROR;
}
boolean warning = false;
for (String line : outputAnalyzer.getOutput().lines()
.toArray(String[]::new)) {
if (line.matches(Test.ERROR + " ?")) return Status.ERROR;
if (line.matches(Test.WARNING + " ?")) warning = true;
}
return warning ? Status.WARNING : Status.NORMAL;
}
// Determines the status of verifying.
private static Status verifyingStatus(SignItem signItem, VerifyItem
verifyItem, OutputAnalyzer outputAnalyzer) {
List<String> expectedSignedContent = new ArrayList<>();
if (verifyItem.certInfo == null) {
expectedSignedContent.addAll(signItem.jarContents);
} else {
SignItem i = signItem;
while (i != null) {
if (i.certInfo != null && i.certInfo.equals(verifyItem.certInfo)) {
expectedSignedContent.addAll(i.jarContents);
}
i = i.prevSign;
}
}
List<String> expectedUnsignedContent =
new ArrayList<>(signItem.jarContents);
expectedUnsignedContent.removeAll(expectedSignedContent);
int expectedExitCode = !STRICT || expectedUnsignedContent.isEmpty() ? 0 : 32;
if (outputAnalyzer.getExitValue() != expectedExitCode) {
System.out.println("verifyingStatus: error: exit code != " + expectedExitCode + ": " + outputAnalyzer.getExitValue() + " != " + expectedExitCode);
return Status.ERROR;
}
String expectedSuccessMessage = expectedUnsignedContent.isEmpty() ?
Test.JAR_VERIFIED : Test.JAR_VERIFIED_WITH_SIGNER_ERRORS;
if (!outputAnalyzer.getOutput().contains(expectedSuccessMessage)) {
System.out.println("verifyingStatus: error: expectedSuccessMessage not found: " + expectedSuccessMessage);
return Status.ERROR;
}
boolean tsa = signItem.tsaIndex >= 0;
boolean warning = false;
for (String line : outputAnalyzer.getOutput().lines()
.toArray(String[]::new)) {
if (line.isBlank()) {
// If line is blank and warning flag is true, it is the end of warnings section
// This is needed when some info is added after warnings, such as timestamp expiration date
if (warning) warning = false;
continue;
}
if (Test.JAR_VERIFIED.equals(line)) continue;
if (line.matches(Test.ERROR + " ?") && expectedExitCode == 0) {
System.out.println("verifyingStatus: error: line.matches(" + Test.ERROR + "\" ?\"): " + line);
return Status.ERROR;
}
if (line.matches(Test.WARNING + " ?")) {
warning = true;
continue;
}
if (!warning) continue;
line = line.strip();
if (Test.NOT_YET_VALID_CERT_SIGNING_WARNING.equals(line)) continue;
if (Test.HAS_EXPIRING_CERT_SIGNING_WARNING.equals(line)) continue;
if (Test.HAS_EXPIRING_CERT_VERIFYING_WARNING.equals(line)) continue;
if (line.matches("^" + Test.NO_TIMESTAMP_SIGNING_WARN_TEMPLATE
.replaceAll(
"\\(%1\\$tY-%1\\$tm-%1\\$td\\)", "\\\\([^\\\\)]+\\\\)"
+ "( or after any future revocation date)?")
.replaceAll("[.]", "[.]") + "$") && !tsa) continue;
if (line.matches("^" + Test.NO_TIMESTAMP_VERIFYING_WARN_TEMPLATE
.replaceAll("\\(as early as %1\\$tY-%1\\$tm-%1\\$td\\)",
"\\\\([^\\\\)]+\\\\)"
+ "( or after any future revocation date)?")
.replaceAll("[.]", "[.]") + "$") && !tsa) continue;
if (line.matches("^This jar contains signatures that do(es)? not "
+ "include a timestamp[.] Without a timestamp, users may "
+ "not be able to validate this jar after the signer "
+ "certificate's expiration date \\([^\\)]+\\) or after "
+ "any future revocation date[.]") && !tsa) continue;
if (isWeakAlg(signItem.expectedDigestAlg())
&& line.contains(Test.WEAK_ALGORITHM_WARNING)) continue;
if (Test.CERTIFICATE_SELF_SIGNED.equals(line)) continue;
if (Test.HAS_EXPIRED_CERT_VERIFYING_WARNING.equals(line)
&& signItem.certInfo.expired) continue;
System.out.println("verifyingStatus: unexpected line: " + line);
return Status.ERROR; // treat unexpected warnings as error
}
return warning ? Status.WARNING : Status.NORMAL;
}
private static boolean isWeakAlg(String alg) {
return SHA1.equals(alg);
}
// Using specified jarsigner to sign the pre-created jar with specified
// algorithms.
private static OutputAnalyzer signJar(String jarsignerPath, String sigalg,
String jarDigestAlgorithm,
String tsadigestalg, String tsa, String alias, String unsignedJar,
String signedJar) throws Throwable {
List<String> 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("-debug");
arguments.add("-verbose");
if (jarDigestAlgorithm != null) {
arguments.add("-digestalg");
arguments.add(jarDigestAlgorithm);
}
if (sigalg != null) {
arguments.add("-sigalg");
arguments.add(sigalg);
}
if (tsa != null) {
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("-sigfile");
arguments.add(nextSigfileName(alias, unsignedJar, signedJar));
arguments.add("-signedjar");
arguments.add(signedJar + ".jar");
arguments.add(unsignedJar + ".jar");
arguments.add(alias);
OutputAnalyzer outputAnalyzer = execTool(jarsignerPath,
arguments.toArray(new String[arguments.size()]));
return outputAnalyzer;
}
// Using specified jarsigner to verify the signed jar.
private static OutputAnalyzer verifyJar(String jarsignerPath,
String signedJar, String alias) throws Throwable {
List<String> arguments = new ArrayList<>();
arguments.add("-J-Djava.security.properties=" + JAVA_SECURITY);
arguments.add("-debug");
arguments.add("-verbose");
arguments.add("-certs");
arguments.add("-keystore");
arguments.add(KEYSTORE);
arguments.add("-verify");
if (STRICT) arguments.add("-strict");
arguments.add(signedJar + ".jar");
if (alias != null) arguments.add(alias);
OutputAnalyzer outputAnalyzer = execTool(jarsignerPath,
arguments.toArray(new String[arguments.size()]));
return outputAnalyzer;
}
// Generates the test result report.
private static boolean generateReport(List<JdkInfo> jdkList, List<TsaInfo> tsaList,
List<SignItem> signItems) throws IOException {
System.out.println("Report is being generated...");
StringBuilder report = new StringBuilder();
report.append(HtmlHelper.startHtml());
report.append(HtmlHelper.startPre());
// Generates JDK list
report.append("JDK list:\n");
for(JdkInfo jdkInfo : jdkList) {
report.append(String.format("%d=%s%n",
jdkInfo.index,
jdkInfo.runtimeVersion));
}
// Generates TSA URLs
report.append("TSA list:\n");
for(TsaInfo tsaInfo : tsaList) {
report.append(
String.format("%d=%s%n", tsaInfo.index,
tsaInfo.tsaUrl == null ? "notsa" : tsaInfo.tsaUrl));
}
report.append(HtmlHelper.endPre());
report.append(HtmlHelper.startTable());
// Generates report headers.
List<String> headers = new ArrayList<>();
headers.add("[Jarfile]");
headers.add("[Signing Certificate]");
headers.add("[Signer JDK]");
headers.add("[Signature Algorithm]");
headers.add("[Jar Digest Algorithm]");
headers.add("[TSA Digest Algorithm]");
headers.add("[TSA]");
headers.add("[Signing Status]");
headers.add("[Verifier JDK]");
headers.add("[Verifying Certificate]");
headers.add("[Verifying Status]");
if (DELAY_VERIFY) {
headers.add("[Delay Verifying Status]");
}
headers.add("[Failed]");
report.append(HtmlHelper.htmlRow(
headers.toArray(new String[headers.size()])));
StringBuilder failedReport = new StringBuilder(report.toString());
boolean failed = signItems.isEmpty();
// Generates report rows.
for (SignItem signItem : signItems) {
failed = failed || signItem.verifyItems.isEmpty();
for (VerifyItem verifyItem : signItem.verifyItems) {
String reportRow = reportRow(signItem, verifyItem);
report.append(reportRow);
boolean isFailedCase = isFailed(signItem, verifyItem);
if (isFailedCase) {
failedReport.append(reportRow);
}
failed = failed || isFailedCase;
}
}
report.append(HtmlHelper.endTable());
report.append(HtmlHelper.endHtml());
generateFile("report.html", report.toString());
if (failed) {
failedReport.append(HtmlHelper.endTable());
failedReport.append(HtmlHelper.endPre());
failedReport.append(HtmlHelper.endHtml());
generateFile("failedReport.html", failedReport.toString());
}
System.out.println("Report is generated.");
return failed;
}
private static void generateFile(String path, String content)
throws IOException {
FileWriter writer = new FileWriter(new File(path));
writer.write(content);
writer.close();
}
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 {
long start = System.currentTimeMillis();
try {
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);
} finally {
long end = System.currentTimeMillis();
System.out.println("child process duration [ms]: " + (end - start));
}
}
private static class JdkInfo {
private int index;
private final String jdkPath;
private final String jarsignerPath;
private final String runtimeVersion;
private String version;
private final int majorVersion;
private final boolean supportsTsadigestalg;
private Map<String, Boolean> sigalgMap = new HashMap<>();
private JdkInfo(String jdkPath) throws Throwable {
this.jdkPath = jdkPath;
jarsignerPath = jarsignerPath(jdkPath);
runtimeVersion = execJdkUtils(jdkPath, JdkUtils.M_JAVA_RUNTIME_VERSION);
if (runtimeVersion == null || runtimeVersion.isBlank()) {
throw new RuntimeException(
"Cannot determine the JDK version: " + jdkPath);
}
version = execJdkUtils(jdkPath, JdkUtils.M_JAVA_VERSION);
majorVersion = Integer.parseInt((runtimeVersion.matches("^1[.].*") ?
runtimeVersion.substring(2) : runtimeVersion).replaceAll("[^0-9].*$", ""));
supportsTsadigestalg = execTool(jarsignerPath, "-help")
.getOutput().contains("-tsadigestalg");
}
private boolean isSupportedSigalg(String sigalg) throws Throwable {
if (!sigalgMap.containsKey(sigalg)) {
boolean isSupported = Boolean.parseBoolean(
execJdkUtils(
jdkPath,
JdkUtils.M_IS_SUPPORTED_SIGALG,
sigalg));
sigalgMap.put(sigalg, isSupported);
}
return sigalgMap.get(sigalg);
}
private boolean isAtLeastMajorVersion(int minVersion) {
return majorVersion >= minVersion;
}
private boolean supportsKeyAlg(String keyAlgorithm) {
// JDK 6 doesn't support EC
return isAtLeastMajorVersion(6) || !EC.equals(keyAlgorithm);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((runtimeVersion == null) ? 0 : runtimeVersion.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 (runtimeVersion == null) {
if (other.runtimeVersion != null)
return false;
} else if (!runtimeVersion.equals(other.runtimeVersion))
return false;
return true;
}
@Override
public String toString() {
return "JdkInfo[" + runtimeVersion + ", " + jdkPath + "]";
}
}
private static class TsaInfo {
private final int index;
private final String tsaUrl;
private Set<String> digestList = new HashSet<>();
private TsaInfo(int index, String tsa) {
this.index = index;
this.tsaUrl = tsa;
}
private void addDigest(String digest) {
digestList.add(digest);
}
private boolean isDigestSupported(String digest) {
return digest == null || digestList.isEmpty()
|| digestList.contains(digest);
}
@Override
public String toString() {
return "TsaInfo[" + index + ", " + tsaUrl + "]";
}
}
private static class CertInfo {
private static int certCounter;
// nr distinguishes cert CNs in jarsigner -verify output
private final int nr = ++certCounter;
private final JdkInfo jdkInfo;
private final String keyAlgorithm;
private final String digestAlgorithm;
private final int keySize;
private final boolean expired;
private CertInfo(JdkInfo jdkInfo, String keyAlgorithm,
String digestAlgorithm, int keySize, boolean expired) {
this.jdkInfo = jdkInfo;
this.keyAlgorithm = keyAlgorithm;
this.digestAlgorithm = digestAlgorithm;
this.keySize = keySize;
this.expired = expired;
}
private String sigalg() {
return DEFAULT.equals(digestAlgorithm) ? null : expectedSigalg();
}
private String expectedSigalg() {
return (DEFAULT.equals(this.digestAlgorithm) ? this.digestAlgorithm
: "SHA-256").replace("-", "") + "with" +
keyAlgorithm + (EC.equals(keyAlgorithm) ? "DSA" : "");
}
private int expectedKeySize() {
if (keySize != 0) return keySize;
// defaults
if (RSA.equals(keyAlgorithm) || DSA.equals(keyAlgorithm)) {
return 3072;
} else if (EC.equals(keyAlgorithm)) {
return 384;
} else {
throw new RuntimeException("problem determining key size");
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ (digestAlgorithm == null ? 0 : digestAlgorithm.hashCode());
result = prime * result + (expired ? 1231 : 1237);
result = prime * result
+ (jdkInfo == null ? 0 : jdkInfo.hashCode());
result = prime * result
+ (keyAlgorithm == null ? 0 : keyAlgorithm.hashCode());
result = prime * result + keySize;
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 (digestAlgorithm == null) {
if (other.digestAlgorithm != null)
return false;
} else if (!digestAlgorithm.equals(other.digestAlgorithm))
return false;
if (expired != other.expired)
return false;
if (jdkInfo == null) {
if (other.jdkInfo != null)
return false;
} else if (!jdkInfo.equals(other.jdkInfo))
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;
return true;
}
private String alias() {
return (jdkInfo.version + "_" + toString())
// lower case for jks due to
// sun.security.provider.JavaKeyStore.JDK.convertAlias
.toLowerCase(Locale.ENGLISH);
}
@Override
public String toString() {
return "nr" + nr + "_"
+ keyAlgorithm + "_" + digestAlgorithm
+ (keySize == 0 ? "" : "_" + keySize)
+ (expired ? "_Expired" : "");
}
}
// It does only one timestamping for the same JDK, digest algorithm and
// TSA service with an arbitrary valid/expired certificate.
private static class TsaFilter {
private static final Set<Condition> SET = new HashSet<>();
private static boolean filter(String signerVersion,
String digestAlgorithm, boolean expiredCert, int tsaIndex) {
return !SET.add(new Condition(signerVersion, digestAlgorithm,
expiredCert, tsaIndex));
}
private static class Condition {
private final String signerVersion;
private final String digestAlgorithm;
private final boolean expiredCert;
private final int tsaIndex;
private Condition(String signerVersion, String digestAlgorithm,
boolean expiredCert, int tsaIndex) {
this.signerVersion = signerVersion;
this.digestAlgorithm = digestAlgorithm;
this.expiredCert = expiredCert;
this.tsaIndex = tsaIndex;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((digestAlgorithm == null) ? 0 : digestAlgorithm.hashCode());
result = prime * result + (expiredCert ? 1231 : 1237);
result = prime * result
+ ((signerVersion == null) ? 0 : signerVersion.hashCode());
result = prime * result + tsaIndex;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Condition other = (Condition) obj;
if (digestAlgorithm == null) {
if (other.digestAlgorithm != null)
return false;
} else if (!digestAlgorithm.equals(other.digestAlgorithm))
return false;
if (expiredCert != other.expiredCert)
return false;
if (signerVersion == null) {
if (other.signerVersion != null)
return false;
} else if (!signerVersion.equals(other.signerVersion))
return false;
if (tsaIndex != other.tsaIndex)
return false;
return true;
}
}}
private static enum Status {
// No action due to pre-action fails.
NONE,
// 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 SignItem prevSign;
private CertInfo certInfo;
private JdkInfo jdkInfo;
private String digestAlgorithm;
private String tsaDigestAlgorithm;
private int tsaIndex;
private Status status;
private String unsignedJar;
private String signedJar;
private List<String> jarContents = new ArrayList<>();
private List<VerifyItem> verifyItems = new ArrayList<>();
private static SignItem build() {
return new SignItem()
.addContentFiles(Arrays.asList("META-INF/MANIFEST.MF"));
}
private static SignItem build(SignItem prevSign) {
return build().prevSign(prevSign).unsignedJar(prevSign.signedJar)
.addContentFiles(prevSign.jarContents);
}
private SignItem prevSign(SignItem prevSign) {
this.prevSign = prevSign;
return this;
}
private SignItem certInfo(CertInfo certInfo) {
this.certInfo = certInfo;
return this;
}
private SignItem jdkInfo(JdkInfo jdkInfo) {
this.jdkInfo = jdkInfo;
return this;
}
private SignItem digestAlgorithm(String digestAlgorithm) {
this.digestAlgorithm = digestAlgorithm;
return this;
}
String expectedDigestAlg() {
return digestAlgorithm != null ? digestAlgorithm : "SHA-256";
}
private SignItem tsaDigestAlgorithm(String tsaDigestAlgorithm) {
this.tsaDigestAlgorithm = tsaDigestAlgorithm;
return this;
}
String expectedTsaDigestAlg() {
return tsaDigestAlgorithm != null ? tsaDigestAlgorithm : "SHA-256";
}
private SignItem tsaIndex(int tsaIndex) {
this.tsaIndex = tsaIndex;
return this;
}
private SignItem status(Status status) {
this.status = status;
return this;
}
private SignItem unsignedJar(String unsignedJar) {
this.unsignedJar = unsignedJar;
return this;
}
private SignItem signedJar(String signedJar) {
this.signedJar = signedJar;
return this;
}
private SignItem addContentFiles(List<String> files) {
this.jarContents.addAll(files);
return this;
}
private void addVerifyItem(VerifyItem verifyItem) {
verifyItems.add(verifyItem);
}
private boolean isErrorInclPrev() {
if (prevSign != null && prevSign.isErrorInclPrev()) {
System.out.println("SignItem.isErrorInclPrev: returning true from previous");
return true;
}
return status == Status.ERROR;
}
private List<String> toStringWithPrev(Function<SignItem,String> toStr) {
List<String> s = new ArrayList<>();
if (prevSign != null) {
s.addAll(prevSign.toStringWithPrev(toStr));
}
if (status != null) { // no status means jar creation or update item
s.add(toStr.apply(this));
}
return s;
}
}
private static class VerifyItem {
private VerifyItem prevVerify;
private CertInfo certInfo;
private JdkInfo jdkInfo;
private Status status = Status.NONE;
private Status delayStatus = Status.NONE;
private static VerifyItem build(JdkInfo jdkInfo) {
VerifyItem verifyItem = new VerifyItem();
verifyItem.jdkInfo = jdkInfo;
return verifyItem;
}
private VerifyItem certInfo(CertInfo certInfo) {
this.certInfo = certInfo;
return this;
}
private void addSignerCertInfos(SignItem signItem) {
VerifyItem prevVerify = this;
CertInfo lastCertInfo = null;
while (signItem != null) {
// (signItem.certInfo == null) means create or update jar step
if (signItem.certInfo != null
&& !signItem.certInfo.equals(lastCertInfo)) {
lastCertInfo = signItem.certInfo;
prevVerify = prevVerify.prevVerify =
build(jdkInfo).certInfo(signItem.certInfo);
}
signItem = signItem.prevSign;
}
}
private VerifyItem status(Status status) {
this.status = status;
return this;
}
private boolean isErrorInclPrev() {
if (prevVerify != null && prevVerify.isErrorInclPrev()) {
System.out.println("VerifyItem.isErrorInclPrev: returning true from previous");
return true;
}
return status == Status.ERROR || delayStatus == Status.ERROR;
}
private VerifyItem delayStatus(Status status) {
this.delayStatus = status;
return this;
}
private List<String> toStringWithPrev(
Function<VerifyItem,String> toStr) {
List<String> s = new ArrayList<>();
if (prevVerify != null) {
s.addAll(prevVerify.toStringWithPrev(toStr));
}
s.add(toStr.apply(this));
return s;
}
}
// The identifier for a specific signing.
private static String signingId(SignItem signItem) {
return signItem.signedJar;
}
// The identifier for a specific verifying.
private static String verifyingId(SignItem signItem, VerifyItem verifyItem,
boolean delayVerify) {
return signingId(signItem) + (delayVerify ? "-DV" : "-V")
+ "_" + verifyItem.jdkInfo.version +
(verifyItem.certInfo == null ? "" : "_" + verifyItem.certInfo);
}
private static String reportRow(SignItem signItem, VerifyItem verifyItem) {
List<String> values = new ArrayList<>();
Consumer<Function<SignItem, String>> s_values_add = f -> {
values.add(String.join("<br/><br/>", signItem.toStringWithPrev(f)));
};
Consumer<Function<VerifyItem, String>> v_values_add = f -> {
values.add(String.join("<br/><br/>", verifyItem.toStringWithPrev(f)));
};
s_values_add.accept(i -> i.unsignedJar + " -> " + i.signedJar);
s_values_add.accept(i -> i.certInfo.toString());
s_values_add.accept(i -> i.jdkInfo.version);
s_values_add.accept(i -> i.certInfo.expectedSigalg());
s_values_add.accept(i ->
null2Default(i.digestAlgorithm, i.expectedDigestAlg()));
s_values_add.accept(i -> i.tsaIndex == -1 ? "" :
null2Default(i.tsaDigestAlgorithm, i.expectedTsaDigestAlg()));
s_values_add.accept(i -> i.tsaIndex == -1 ? "" : i.tsaIndex + "");
s_values_add.accept(i -> HtmlHelper.anchorLink(
PhaseOutputStream.fileName(PhaseOutputStream.Phase.SIGNING),
signingId(i),
"" + i.status));
values.add(verifyItem.jdkInfo.version);
v_values_add.accept(i ->
i.certInfo == null ? "no alias" : "" + i.certInfo);
v_values_add.accept(i -> HtmlHelper.anchorLink(
PhaseOutputStream.fileName(PhaseOutputStream.Phase.VERIFYING),
verifyingId(signItem, i, false),
"" + i.status.toString()));
if (DELAY_VERIFY) {
v_values_add.accept(i -> HtmlHelper.anchorLink(
PhaseOutputStream.fileName(
PhaseOutputStream.Phase.DELAY_VERIFYING),
verifyingId(signItem, verifyItem, true),
verifyItem.delayStatus.toString()));
}
values.add(isFailed(signItem, verifyItem) ? "X" : "");
return HtmlHelper.htmlRow(values.toArray(new String[values.size()]));
}
private static boolean isFailed(SignItem signItem, VerifyItem verifyItem) {
System.out.println("isFailed: signItem = " + signItem + ", verifyItem = " + verifyItem);
// TODO: except known failing cases
// Note about isAtLeastMajorVersion in the following conditions:
// signItem.jdkInfo is the jdk which signed the jar last and
// signItem.prevSign.jdkInfo is the jdk which signed the jar first
// assuming only two successive signatures as there actually are now.
// the first signature always works and always has. subject here is
// the update of an already signed jar. the following conditions always
// depend on the second jdk that updated the jar with another signature
// and the first one (signItem(.prevSign)+.jdkInfo) can be ignored.
// this is different for verifyItem. verifyItem.prevVerify refers to
// the first signature created by signItem(.prevSign)+.jdkInfo.
// all verifyItem(.prevVerify)+.jdkInfo however point always to the same
// jdk, only their certInfo is different. the same signatures are
// verified with different jdks in different top-level VerifyItems
// attached directly to signItem.verifyItems and not to
// verifyItem.prevVerify.
// ManifestDigester fails to parse manifests ending in '\r' with
// IndexOutOfBoundsException at ManifestDigester.java:87 before 8217375
if (signItem.signedJar.startsWith("eofr")
&& !signItem.jdkInfo.isAtLeastMajorVersion(13)
&& !verifyItem.jdkInfo.isAtLeastMajorVersion(13)) return false;
// if there is no blank line after main attributes, JarSigner adds
// individual sections nevertheless without being properly delimited
// in JarSigner.java:777..790 without checking for blank line
// before 8217375
// if (signItem.signedJar.startsWith("eofn-")
// && signItem.signedJar.contains("-addfile-")
// && !signItem.jdkInfo.isAtLeastMajorVersion(13)
// && !verifyItem.jdkInfo.isAtLeastMajorVersion(13)) return false; // FIXME
// System.out.println("isFailed: signItem.isErrorInclPrev() " + signItem.isErrorInclPrev());
// System.out.println("isFailed: verifyItem.isErrorInclPrev() " + verifyItem.isErrorInclPrev());
boolean isFailed = signItem.isErrorInclPrev() || verifyItem.isErrorInclPrev();
System.out.println("isFailed: returning " + isFailed);
return isFailed;
}
// If a value is null, then displays the default value or N/A.
private static String null2Default(String value, String defaultValue) {
return value != null ? value :
DEFAULT + "(" + (defaultValue == null
? "N/A"
: defaultValue) + ")";
}
}