1 /*
2  * Copyright (c) 2017, 2021, 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 8217375 8260286
27  * @summary This test is used to verify the compatibility of jarsigner across
28  *     different JDK releases. It also can be used to check jar signing (w/
29  *     and w/o TSA) and to verify some specific signing and digest algorithms.
30  *     Note that this is a manual test. For more details about the test and
31  *     its usages, please look through the README.
32  *
33  * @library /test/lib ../warnings
34  * @compile -source 1.7 -target 1.7 JdkUtils.java
35  * @run main/manual/othervm Compatibility
36  */
37 
38 import static java.nio.charset.StandardCharsets.UTF_8;
39 
40 import java.io.BufferedReader;
41 import java.io.File;
42 import java.io.FileOutputStream;
43 import java.io.FileReader;
44 import java.io.FileWriter;
45 import java.io.IOException;
46 import java.io.OutputStream;
47 import java.io.PrintStream;
48 import java.nio.file.Files;
49 import java.nio.file.Path;
50 import java.text.DateFormat;
51 import java.text.SimpleDateFormat;
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.Calendar;
55 import java.util.Date;
56 import java.util.HashMap;
57 import java.util.HashSet;
58 import java.util.List;
59 import java.util.Locale;
60 import java.util.Map;
61 import java.util.Set;
62 import java.util.concurrent.TimeUnit;
63 import java.util.function.Consumer;
64 import java.util.function.Function;
65 import java.util.jar.Attributes.Name;
66 import java.util.jar.Manifest;
67 import java.util.stream.Collectors;
68 import java.util.stream.IntStream;
69 
70 import jdk.test.lib.process.OutputAnalyzer;
71 import jdk.test.lib.process.ProcessTools;
72 import jdk.test.lib.util.JarUtils;
73 
74 public class Compatibility {
75 
76     private static final String TEST_SRC = System.getProperty("test.src");
77     private static final String TEST_CLASSES = System.getProperty("test.classes");
78     private static final String TEST_JDK = System.getProperty("test.jdk");
79     private static JdkInfo TEST_JDK_INFO;
80 
81     private static final String PROXY_HOST = System.getProperty("proxyHost");
82     private static final String PROXY_PORT = System.getProperty("proxyPort", "80");
83 
84     // An alternative security properties file.
85     // The test provides a default one, which only contains two lines:
86     // jdk.certpath.disabledAlgorithms=MD2, MD5
87     // jdk.jar.disabledAlgorithms=MD2, MD5
88     private static final String JAVA_SECURITY = System.getProperty(
89             "javaSecurityFile", TEST_SRC + "/java.security");
90 
91     private static final String PASSWORD = "testpass";
92     private static final String KEYSTORE = "testKeystore.jks";
93 
94     private static final String RSA = "RSA";
95     private static final String DSA = "DSA";
96     private static final String EC = "EC";
97     private static String[] KEY_ALGORITHMS;
98     private static final String[] DEFAULT_KEY_ALGORITHMS = new String[] {
99             RSA,
100             DSA,
101             EC};
102 
103     private static final String SHA1 = "SHA-1";
104     private static final String SHA256 = "SHA-256";
105     private static final String SHA384 = "SHA-384";
106     private static final String SHA512 = "SHA-512";
107     private static final String DEFAULT = "DEFAULT";
108     private static String[] DIGEST_ALGORITHMS;
109     private static final String[] DEFAULT_DIGEST_ALGORITHMS = new String[] {
110             SHA1,
111             SHA256,
112             SHA384,
113             SHA512, // note: digests break onto continuation line in manifest
114             DEFAULT};
115 
116     private static final boolean[] EXPIRED =
117             Boolean.valueOf(System.getProperty("expired", "true")) ?
118                     new boolean[] { false, true } : new boolean[] { false };
119 
120     private static final boolean TEST_COMPREHENSIVE_JAR_CONTENTS =
121             Boolean.valueOf(System.getProperty(
122                     "testComprehensiveJarContents", "false"));
123 
124     private static final boolean TEST_JAR_UPDATE =
125             Boolean.valueOf(System.getProperty("testJarUpdate", "false"));
126 
127     private static final boolean STRICT =
128             Boolean.valueOf(System.getProperty("strict", "false"));
129 
130     private static final Calendar CALENDAR = Calendar.getInstance();
131     private static final DateFormat DATE_FORMAT
132             = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
133 
134     // The certificate validity period in minutes. The default value is 1440
135     // minutes, namely 1 day.
136     private static final int CERT_VALIDITY
137             = Integer.valueOf(System.getProperty("certValidity", "1440"));
138     static {
139         if (CERT_VALIDITY < 1 || CERT_VALIDITY > 1440) {
140             throw new RuntimeException(
141                     "certValidity out of range [1, 1440]: " + CERT_VALIDITY);
142         }
143     }
144 
145     // If true, an additional verifying will be triggered after all of
146     // valid certificates expire. The default value is false.
147     public static final boolean DELAY_VERIFY
148             = Boolean.valueOf(System.getProperty("delayVerify", "false"));
149 
150     private static long lastCertStartTime;
151 
152     private static DetailsOutputStream detailsOutput;
153 
154     private static int sigfileCounter;
155 
nextSigfileName(String alias, String u, String s)156     private static String nextSigfileName(String alias, String u, String s) {
157         String sigfileName = "" + (++sigfileCounter);
158         System.out.println("using sigfile " + sigfileName + " for alias "
159                     + alias + " signing " + u + ".jar to " + s + ".jar");
160         return sigfileName;
161     }
162 
main(String... args)163     public static void main(String... args) throws Throwable {
164         // Backups stdout and stderr.
165         PrintStream origStdOut = System.out;
166         PrintStream origStdErr = System.err;
167 
168         detailsOutput = new DetailsOutputStream(outfile());
169 
170         // Redirects the system output to a custom one.
171         PrintStream printStream = new PrintStream(detailsOutput);
172         System.setOut(printStream);
173         System.setErr(printStream);
174 
175         TEST_JDK_INFO = new JdkInfo(TEST_JDK);
176 
177         List<TsaInfo> tsaList = tsaInfoList();
178         List<JdkInfo> jdkInfoList = jdkInfoList();
179         List<CertInfo> certList = createCertificates(jdkInfoList);
180         List<SignItem> signItems =
181                 test(jdkInfoList, tsaList, certList, createJars());
182 
183         boolean failed = generateReport(jdkInfoList, tsaList, signItems);
184 
185         // Restores the original stdout and stderr.
186         System.setOut(origStdOut);
187         System.setErr(origStdErr);
188 
189         if (failed) {
190             throw new RuntimeException("At least one test case failed. "
191                     + "Please check the failed row(s) in report.html "
192                     + "or failedReport.html.");
193         }
194     }
195 
createJarFile(String jar, Manifest m, String... files)196     private static SignItem createJarFile(String jar, Manifest m,
197             String... files) throws IOException {
198         JarUtils.createJarFile(Path.of(jar), m, Path.of("."),
199                 Arrays.stream(files).map(Path::of).toArray(Path[]::new));
200         return SignItem.build()
201                 .signedJar(jar.replaceAll("[.]jar$", ""))
202             .addContentFiles(Arrays.stream(files).collect(Collectors.toList()));
203     }
204 
createDummyFile(String name)205     private static String createDummyFile(String name) throws IOException {
206         if (name.contains("/")) new File(name).getParentFile().mkdir();
207         try (OutputStream fos = new FileOutputStream(name)) {
208             fos.write(name.getBytes(UTF_8));
209         }
210         return name;
211     }
212 
213     // Creates one or more jar files to test
createJars()214     private static List<SignItem> createJars() throws IOException {
215         List<SignItem> jarList = new ArrayList<>();
216 
217         Manifest m = new Manifest();
218         m.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0");
219 
220         // creates a jar file that contains a dummy file
221         jarList.add(createJarFile("test.jar", m, createDummyFile("dummy")));
222 
223         if (TEST_COMPREHENSIVE_JAR_CONTENTS) {
224 
225             // empty jar file so that jarsigner will add a default manifest
226             jarList.add(createJarFile("empty.jar", m));
227 
228             // jar file that contains only an empty manifest with empty main
229             // attributes (due to missing "Manifest-Version" header)
230             JarUtils.createJar("nomainatts.jar");
231             jarList.add(SignItem.build().signedJar("nomainatts"));
232 
233             // creates a jar file that contains several files.
234             jarList.add(createJarFile("files.jar", m,
235                     IntStream.range(1, 9).boxed().map(i -> {
236                         try {
237                             return createDummyFile("dummy" + i);
238                         } catch (IOException e) {
239                             throw new RuntimeException(e);
240                         }
241                     }).toArray(String[]::new)
242             ));
243 
244             // forces a line break by exceeding the line width limit of 72 bytes
245             // in the filename and hence manifest entry name
246             jarList.add(createJarFile("longfilename.jar", m,
247                     createDummyFile("test".repeat(20))));
248 
249             // another interesting case is with different digest algorithms
250             // resulting in digests broken across line breaks onto continuation
251             // lines. these however are set with the 'digestAlgs' option or
252             // include all digest algorithms by default, see SignTwice.java.
253         }
254 
255         return jarList;
256     }
257 
258     // updates a signed jar file by adding another file
updateJar(SignItem prev)259     private static List<SignItem> updateJar(SignItem prev) throws IOException {
260         List<SignItem> jarList = new ArrayList<>();
261 
262         // sign unmodified jar again
263         Files.copy(Path.of(prev.signedJar + ".jar"),
264                 Path.of(prev.signedJar + "-signagainunmodified.jar"));
265         jarList.add(SignItem.build(prev)
266                 .signedJar(prev.signedJar + "-signagainunmodified"));
267 
268         String oldJar = prev.signedJar;
269         String newJar = oldJar + "-addfile";
270         String triggerUpdateFile = "addfile";
271         JarUtils.updateJar(oldJar + ".jar", newJar + ".jar", triggerUpdateFile);
272         jarList.add(SignItem.build(prev).signedJar(newJar)
273                 .addContentFiles(Arrays.asList(triggerUpdateFile)));
274 
275         return jarList;
276     }
277 
278     // Creates a key store that includes a set of valid/expired certificates
279     // with various algorithms.
createCertificates(List<JdkInfo> jdkInfoList)280     private static List<CertInfo> createCertificates(List<JdkInfo> jdkInfoList)
281             throws Throwable {
282         List<CertInfo> certList = new ArrayList<>();
283         Set<String> expiredCertFilter = new HashSet<>();
284 
285         for (JdkInfo jdkInfo : jdkInfoList) {
286             for (String keyAlgorithm : keyAlgs()) {
287                 if (!jdkInfo.supportsKeyAlg(keyAlgorithm)) continue;
288                 for (int keySize : keySizes(keyAlgorithm)) {
289                     for (String digestAlgorithm : digestAlgs()) {
290                         for(boolean expired : EXPIRED) {
291                             // It creates only one expired certificate for one
292                             // key algorithm.
293                             if (expired
294                                     && !expiredCertFilter.add(keyAlgorithm)) {
295                                 continue;
296                             }
297 
298                             CertInfo certInfo = new CertInfo(
299                                     jdkInfo,
300                                     keyAlgorithm,
301                                     digestAlgorithm,
302                                     keySize,
303                                     expired);
304                             // If the signature algorithm is not supported by the
305                             // JDK, it cannot try to sign jar with this algorithm.
306                             String sigalg = certInfo.sigalg();
307                             if (sigalg != null &&
308                                     !jdkInfo.isSupportedSigalg(sigalg)) {
309                                 continue;
310                             }
311                             createCertificate(jdkInfo, certInfo);
312                             certList.add(certInfo);
313                         }
314                     }
315                 }
316             }
317         }
318 
319         System.out.println("the keystore contents:");
320         for (JdkInfo jdkInfo : jdkInfoList) {
321             execTool(jdkInfo.jdkPath + "/bin/keytool", new String[] {
322                     "-v",
323                     "-storetype",
324                     "jks",
325                     "-storepass",
326                     PASSWORD,
327                     "-keystore",
328                     KEYSTORE,
329                     "-list"
330             });
331         }
332 
333         return certList;
334     }
335 
336     // Creates/Updates a key store that adds a certificate with specific algorithm.
createCertificate(JdkInfo jdkInfo, CertInfo certInfo)337     private static void createCertificate(JdkInfo jdkInfo, CertInfo certInfo)
338             throws Throwable {
339         List<String> arguments = new ArrayList<>();
340         arguments.add("-J-Djava.security.properties=" + JAVA_SECURITY);
341         arguments.add("-v");
342         arguments.add("-debug");
343         arguments.add("-storetype");
344         arguments.add("jks");
345         arguments.add("-keystore");
346         arguments.add(KEYSTORE);
347         arguments.add("-storepass");
348         arguments.add(PASSWORD);
349         arguments.add(jdkInfo.majorVersion < 6 ? "-genkey" : "-genkeypair");
350         arguments.add("-keyalg");
351         arguments.add(certInfo.keyAlgorithm);
352         String sigalg = certInfo.sigalg();
353         if (sigalg != null) {
354             arguments.add("-sigalg");
355             arguments.add(sigalg);
356         }
357         if (certInfo.keySize != 0) {
358             arguments.add("-keysize");
359             arguments.add(certInfo.keySize + "");
360         }
361         arguments.add("-dname");
362         arguments.add("CN=" + certInfo);
363         arguments.add("-alias");
364         arguments.add(certInfo.alias());
365         arguments.add("-keypass");
366         arguments.add(PASSWORD);
367 
368         arguments.add("-startdate");
369         arguments.add(startDate(certInfo.expired));
370         arguments.add("-validity");
371 //        arguments.add(DELAY_VERIFY ? "1" : "222"); // > six months no warn
372         arguments.add("1");
373 
374         OutputAnalyzer outputAnalyzer = execTool(
375                 jdkInfo.jdkPath + "/bin/keytool",
376                 arguments.toArray(new String[arguments.size()]));
377         if (outputAnalyzer.getExitValue() != 0
378                 || outputAnalyzer.getOutput().matches("[Ee]xception")
379                 || outputAnalyzer.getOutput().matches(Test.ERROR + " ?")) {
380             System.out.println(outputAnalyzer.getOutput());
381             throw new Exception("error generating a key pair: " + arguments);
382         }
383     }
384 
385     // The validity period of a certificate always be 1 day. For creating an
386     // expired certificate, the start date is the time before 1 day, then the
387     // certificate expires immediately. And for creating a valid certificate,
388     // the start date is the time before (1 day - CERT_VALIDITY minutes), then
389     // the certificate will expires in CERT_VALIDITY minutes.
390     private static String startDate(boolean expiredCert) {
391         CALENDAR.setTime(new Date());
392         if (DELAY_VERIFY || expiredCert) {
393             // corresponds to '-validity 1'
394             CALENDAR.add(Calendar.DAY_OF_MONTH, -1);
395         }
396         if (DELAY_VERIFY && !expiredCert) {
397             CALENDAR.add(Calendar.MINUTE, CERT_VALIDITY);
398         }
399         Date startDate = CALENDAR.getTime();
400         if (!expiredCert) {
401             lastCertStartTime = startDate.getTime();
402         }
403         return DATE_FORMAT.format(startDate);
404     }
405 
406     private static String outfile() {
407         return System.getProperty("o");
408     }
409 
410     // Retrieves JDK info from the file which is specified by property
411     // jdkListFile, or from property jdkList if jdkListFile is not available.
412     private static List<JdkInfo> jdkInfoList() throws Throwable {
413         String[] jdkList = list("jdkList");
414         if (jdkList.length == 0) {
415             jdkList = new String[] { "TEST_JDK" };
416         }
417 
418         List<JdkInfo> jdkInfoList = new ArrayList<>();
419         int index = 0;
420         for (String jdkPath : jdkList) {
421             JdkInfo jdkInfo = "TEST_JDK".equalsIgnoreCase(jdkPath) ?
422                     TEST_JDK_INFO : new JdkInfo(jdkPath);
423             // The JDK version must be unique.
424             if (!jdkInfoList.contains(jdkInfo)) {
425                 jdkInfo.index = index++;
426                 jdkInfo.version = String.format(
427                         "%s(%d)", jdkInfo.version, jdkInfo.index);
428                 jdkInfoList.add(jdkInfo);
429             } else {
430                 System.out.println("The JDK version is duplicate: " + jdkPath);
431             }
432         }
433         return jdkInfoList;
434     }
435 
436     private static List<String> keyAlgs() throws IOException {
437         if (KEY_ALGORITHMS == null) KEY_ALGORITHMS = list("keyAlgs");
438         if (KEY_ALGORITHMS.length == 0)
439             return Arrays.asList(DEFAULT_KEY_ALGORITHMS);
440         return Arrays.stream(KEY_ALGORITHMS).map(a -> a.split(";")[0])
441                 .collect(Collectors.toList());
442     }
443 
444     // Return key sizes according to the specified key algorithm.
keySizes(String keyAlgorithm)445     private static int[] keySizes(String keyAlgorithm) throws IOException {
446         if (KEY_ALGORITHMS == null) KEY_ALGORITHMS = list("keyAlgs");
447         for (String keyAlg : KEY_ALGORITHMS) {
448             String[] split = (keyAlg + " ").split(";");
449             if (keyAlgorithm.equals(split[0].trim()) && split.length > 1) {
450                 int sizes[] = new int[split.length - 1];
451                 for (int i = 1; i <= sizes.length; i++)
452                     sizes[i - 1] = split[i].isBlank() ? 0 : // default
453                         Integer.parseInt(split[i].trim());
454                 return sizes;
455             }
456         }
457 
458         // defaults
459         if (RSA.equals(keyAlgorithm) || DSA.equals(keyAlgorithm)) {
460             return new int[] { 1024, 2048, 0 }; // 0 is no keysize specified
461         } else if (EC.equals(keyAlgorithm)) {
462             return new int[] { 384, 571, 0 }; // 0 is no keysize specified
463         } else {
464             throw new RuntimeException("problem determining key sizes");
465         }
466     }
467 
digestAlgs()468     private static List<String> digestAlgs() throws IOException {
469         if (DIGEST_ALGORITHMS == null) DIGEST_ALGORITHMS = list("digestAlgs");
470         if (DIGEST_ALGORITHMS.length == 0)
471             return Arrays.asList(DEFAULT_DIGEST_ALGORITHMS);
472         return Arrays.asList(DIGEST_ALGORITHMS);
473     }
474 
475     // Retrieves TSA info from the file which is specified by property tsaListFile,
476     // or from property tsaList if tsaListFile is not available.
tsaInfoList()477     private static List<TsaInfo> tsaInfoList() throws IOException {
478         String[] tsaList = list("tsaList");
479 
480         List<TsaInfo> tsaInfoList = new ArrayList<>();
481         for (int i = 0; i < tsaList.length; i++) {
482             String[] values = tsaList[i].split(";digests=");
483 
484             String[] digests = new String[0];
485             if (values.length == 2) {
486                 digests = values[1].split(",");
487             }
488 
489             String tsaUrl = values[0];
490             if (tsaUrl.isEmpty() || tsaUrl.equalsIgnoreCase("notsa")) {
491                 tsaUrl = null;
492             }
493             TsaInfo bufTsa = new TsaInfo(i, tsaUrl);
494             for (String digest : digests) {
495                 bufTsa.addDigest(digest.toUpperCase());
496             }
497             tsaInfoList.add(bufTsa);
498         }
499 
500         if (tsaInfoList.size() == 0) {
501             throw new RuntimeException("TSA service is mandatory unless "
502                     + "'notsa' specified explicitly.");
503         }
504         return tsaInfoList;
505     }
506 
list(String listProp)507     private static String[] list(String listProp) throws IOException {
508         String listFileProp = listProp + "File";
509         String listFile = System.getProperty(listFileProp);
510         if (!isEmpty(listFile)) {
511             System.out.println(listFileProp + "=" + listFile);
512             List<String> list = new ArrayList<>();
513             BufferedReader reader = new BufferedReader(
514                     new FileReader(listFile));
515             String line;
516             while ((line = reader.readLine()) != null) {
517                 String item = line.trim();
518                 if (!item.isEmpty()) {
519                     list.add(item);
520                 }
521             }
522             reader.close();
523             return list.toArray(new String[list.size()]);
524         }
525 
526         String list = System.getProperty(listProp);
527         System.out.println(listProp + "=" + list);
528         return !isEmpty(list) ? list.split("#") : new String[0];
529     }
530 
isEmpty(String str)531     private static boolean isEmpty(String str) {
532         return str == null || str.isEmpty();
533     }
534 
535     // A JDK (signer) signs a jar with a variety of algorithms, and then all of
536     // JDKs (verifiers), including the signer itself, try to verify the signed
537     // jars respectively.
test(List<JdkInfo> jdkInfoList, List<TsaInfo> tsaInfoList, List<CertInfo> certList, List<SignItem> jars)538     private static List<SignItem> test(List<JdkInfo> jdkInfoList,
539             List<TsaInfo> tsaInfoList, List<CertInfo> certList,
540             List<SignItem> jars) throws Throwable {
541         detailsOutput.transferPhase();
542         List<SignItem> signItems = new ArrayList<>();
543         signItems.addAll(signing(jdkInfoList, tsaInfoList, certList, jars));
544         if (TEST_JAR_UPDATE) {
545             signItems.addAll(signing(jdkInfoList, tsaInfoList, certList,
546                     updating(signItems.stream().filter(
547                             x -> x.status != Status.ERROR)
548                     .collect(Collectors.toList()))));
549         }
550 
551         detailsOutput.transferPhase();
552         for (SignItem signItem : signItems) {
553             for (JdkInfo verifierInfo : jdkInfoList) {
554                 if (!verifierInfo.supportsKeyAlg(
555                         signItem.certInfo.keyAlgorithm)) continue;
556                 VerifyItem verifyItem = VerifyItem.build(verifierInfo);
557                 verifyItem.addSignerCertInfos(signItem);
558                 signItem.addVerifyItem(verifyItem);
559                 verifying(signItem, verifyItem);
560             }
561         }
562 
563         // if lastCertExpirationTime passed already now, probably some
564         // certificate was already expired during jar signature verification
565         // (jarsigner -verify) and the test should probably be repeated with an
566         // increased validity period -DcertValidity CERT_VALIDITY
567         long lastCertExpirationTime = lastCertStartTime + 24 * 60 * 60 * 1000;
568         if (lastCertExpirationTime < System.currentTimeMillis()) {
569             throw new AssertionError("CERT_VALIDITY (" + CERT_VALIDITY
570                     + " [minutes]) was too short. "
571                     + "Creating and signing the jars took longer, "
572                     + "presumably at least "
573                     + ((lastCertExpirationTime - System.currentTimeMillis())
574                             / 60 * 1000 + CERT_VALIDITY) + " [minutes].");
575         }
576 
577         if (DELAY_VERIFY) {
578             detailsOutput.transferPhase();
579             System.out.print("Waiting for delay verifying");
580             while (System.currentTimeMillis() < lastCertExpirationTime) {
581                 TimeUnit.SECONDS.sleep(30);
582                 System.out.print(".");
583             }
584             System.out.println();
585 
586             System.out.println("Delay verifying starts");
587             for (SignItem signItem : signItems) {
588                 for (VerifyItem verifyItem : signItem.verifyItems) {
589                     verifying(signItem, verifyItem);
590                 }
591             }
592         }
593 
594         detailsOutput.transferPhase();
595         return signItems;
596     }
597 
signing(List<JdkInfo> jdkInfos, List<TsaInfo> tsaList, List<CertInfo> certList, List<SignItem> unsignedJars)598     private static List<SignItem> signing(List<JdkInfo> jdkInfos,
599             List<TsaInfo> tsaList, List<CertInfo> certList,
600             List<SignItem> unsignedJars) throws Throwable {
601         List<SignItem> signItems = new ArrayList<>();
602 
603         for (CertInfo certInfo : certList) {
604             JdkInfo signerInfo = certInfo.jdkInfo;
605             String keyAlgorithm = certInfo.keyAlgorithm;
606             String sigDigestAlgorithm = certInfo.digestAlgorithm;
607             int keySize = certInfo.keySize;
608             boolean expired = certInfo.expired;
609 
610             for (String jarDigestAlgorithm : digestAlgs()) {
611                 if (DEFAULT.equals(jarDigestAlgorithm)) {
612                     jarDigestAlgorithm = null;
613                 }
614 
615                 for (TsaInfo tsaInfo : tsaList) {
616                     String tsaUrl = tsaInfo.tsaUrl;
617 
618                     List<String> tsaDigestAlgs = digestAlgs();
619                     // no point in specifying a tsa digest algorithm
620                     // for no TSA, except maybe it would issue a warning.
621                     if (tsaUrl == null) tsaDigestAlgs = Arrays.asList(DEFAULT);
622                     // If the JDK doesn't support option -tsadigestalg, the
623                     // associated cases can just be ignored.
624                     if (!signerInfo.supportsTsadigestalg) {
625                         tsaDigestAlgs = Arrays.asList(DEFAULT);
626                     }
627                     for (String tsaDigestAlg : tsaDigestAlgs) {
628                         if (DEFAULT.equals(tsaDigestAlg)) {
629                             tsaDigestAlg = null;
630                         } else if (!tsaInfo.isDigestSupported(tsaDigestAlg)) {
631                             // It has to ignore the digest algorithm, which
632                             // is not supported by the TSA server.
633                             continue;
634                         }
635 
636                         if (tsaUrl != null && TsaFilter.filter(
637                                 signerInfo.version,
638                                 tsaDigestAlg,
639                                 expired,
640                                 tsaInfo.index)) {
641                             continue;
642                         }
643 
644                         for (SignItem prevSign : unsignedJars) {
645                             String unsignedJar = prevSign.signedJar;
646 
647                             SignItem signItem = SignItem.build(prevSign)
648                                     .certInfo(certInfo)
649                                     .jdkInfo(signerInfo);
650                             String signedJar = unsignedJar + "-" + "JDK_" + (
651                                     signerInfo.version + "-CERT_" + certInfo).
652                                     replaceAll("[^a-z_0-9A-Z.]+", "-");
653 
654                             if (jarDigestAlgorithm != null) {
655                                 signedJar += "-DIGESTALG_" + jarDigestAlgorithm;
656                                 signItem.digestAlgorithm(jarDigestAlgorithm);
657                             }
658                             if (tsaUrl == null) {
659                                 signItem.tsaIndex(-1);
660                             } else {
661                                 signedJar += "-TSA_" + tsaInfo.index;
662                                 signItem.tsaIndex(tsaInfo.index);
663                                 if (tsaDigestAlg != null) {
664                                     signedJar += "-TSADIGALG_" + tsaDigestAlg;
665                                     signItem.tsaDigestAlgorithm(tsaDigestAlg);
666                                 }
667                             }
668                             signItem.signedJar(signedJar);
669 
670                             String signingId = signingId(signItem);
671                             detailsOutput.writeAnchorName(signingId,
672                                     "Signing: " + signingId);
673 
674                             OutputAnalyzer signOA = signJar(
675                                     signerInfo.jarsignerPath,
676                                     certInfo.sigalg(),
677                                     jarDigestAlgorithm,
678                                     tsaDigestAlg,
679                                     tsaUrl,
680                                     certInfo.alias(),
681                                     unsignedJar,
682                                     signedJar);
683                             Status signingStatus = signingStatus(signOA,
684                                     tsaUrl != null);
685                             signItem.status(signingStatus);
686                             signItems.add(signItem);
687                         }
688                     }
689                 }
690             }
691         }
692 
693         return signItems;
694     }
695 
updating(List<SignItem> prevSignItems)696     private static List<SignItem> updating(List<SignItem> prevSignItems)
697             throws IOException {
698         List<SignItem> updateItems = new ArrayList<>();
699         for (SignItem prevSign : prevSignItems) {
700             updateItems.addAll(updateJar(prevSign));
701         }
702         return updateItems;
703     }
704 
verifying(SignItem signItem, VerifyItem verifyItem)705     private static void verifying(SignItem signItem, VerifyItem verifyItem)
706             throws Throwable {
707         // TODO: how will be ensured that the first verification is not after valid period expired which is only one minute?
708         boolean delayVerify = verifyItem.status != Status.NONE;
709         String verifyingId = verifyingId(signItem, verifyItem, delayVerify);
710         detailsOutput.writeAnchorName(verifyingId, "Verifying: " + verifyingId);
711         OutputAnalyzer verifyOA = verifyJar(verifyItem.jdkInfo.jarsignerPath,
712                 signItem.signedJar, verifyItem.certInfo == null ? null :
713                 verifyItem.certInfo.alias());
714         Status verifyingStatus = verifyingStatus(signItem, verifyItem, verifyOA);
715 
716         try {
717             String match = "^  ("
718                     + "  Signature algorithm: " + signItem.certInfo.
719                             expectedSigalg() + ", " + signItem.certInfo.
720                             expectedKeySize() + "-bit key"
721                     + ")|("
722                     + "  Digest algorithm: " + signItem.expectedDigestAlg()
723                     + (isWeakAlg(signItem.expectedDigestAlg()) ? " \\(weak\\)" : "")
724                     + (signItem.tsaIndex < 0 ? "" :
725                       ")|("
726                     + "Timestamped by \".+\" on .*"
727                     + ")|("
728                     + "  Timestamp digest algorithm: "
729                             + signItem.expectedTsaDigestAlg()
730                     + ")|("
731                     + "  Timestamp signature algorithm: .*"
732                       )
733                     + ")$";
734             verifyOA.stdoutShouldMatchByLine(
735                     "^- Signed by \"CN=" +  signItem.certInfo.toString()
736                             .replaceAll("[.]", "[.]") + "\"$",
737                     "^(- Signed by \"CN=.+\")?$",
738                     match);
739         } catch (Throwable e) {
740             e.printStackTrace();
741             verifyingStatus = Status.ERROR;
742         }
743 
744         if (!delayVerify) {
745             verifyItem.status(verifyingStatus);
746         } else {
747             verifyItem.delayStatus(verifyingStatus);
748         }
749 
750         if (verifyItem.prevVerify != null) {
751             verifying(signItem, verifyItem.prevVerify);
752         }
753     }
754 
755     // Determines the status of signing.
signingStatus(OutputAnalyzer outputAnalyzer, boolean tsa)756     private static Status signingStatus(OutputAnalyzer outputAnalyzer,
757             boolean tsa) {
758         if (outputAnalyzer.getExitValue() != 0) {
759             return Status.ERROR;
760         }
761         if (!outputAnalyzer.getOutput().contains(Test.JAR_SIGNED)) {
762             return Status.ERROR;
763         }
764 
765         boolean warning = false;
766         for (String line : outputAnalyzer.getOutput().lines()
767                 .toArray(String[]::new)) {
768             if (line.matches(Test.ERROR + " ?")) return Status.ERROR;
769             if (line.matches(Test.WARNING + " ?")) warning = true;
770         }
771         return warning ? Status.WARNING : Status.NORMAL;
772     }
773 
774     // Determines the status of verifying.
verifyingStatus(SignItem signItem, VerifyItem verifyItem, OutputAnalyzer outputAnalyzer)775     private static Status verifyingStatus(SignItem signItem, VerifyItem
776             verifyItem, OutputAnalyzer outputAnalyzer) {
777         List<String> expectedSignedContent = new ArrayList<>();
778         if (verifyItem.certInfo == null) {
779             expectedSignedContent.addAll(signItem.jarContents);
780         } else {
781             SignItem i = signItem;
782             while (i != null) {
783                 if (i.certInfo != null && i.certInfo.equals(verifyItem.certInfo)) {
784                     expectedSignedContent.addAll(i.jarContents);
785                 }
786                 i = i.prevSign;
787             }
788         }
789         List<String> expectedUnsignedContent =
790                 new ArrayList<>(signItem.jarContents);
791         expectedUnsignedContent.removeAll(expectedSignedContent);
792 
793         int expectedExitCode = !STRICT || expectedUnsignedContent.isEmpty() ? 0 : 32;
794         if (outputAnalyzer.getExitValue() != expectedExitCode) {
795             System.out.println("verifyingStatus: error: exit code != " + expectedExitCode + ": " + outputAnalyzer.getExitValue() + " != " + expectedExitCode);
796             return Status.ERROR;
797         }
798         String expectedSuccessMessage = expectedUnsignedContent.isEmpty() ?
799                 Test.JAR_VERIFIED : Test.JAR_VERIFIED_WITH_SIGNER_ERRORS;
800         if (!outputAnalyzer.getOutput().contains(expectedSuccessMessage)) {
801             System.out.println("verifyingStatus: error: expectedSuccessMessage not found: " + expectedSuccessMessage);
802             return Status.ERROR;
803         }
804 
805         boolean tsa = signItem.tsaIndex >= 0;
806         boolean warning = false;
807         for (String line : outputAnalyzer.getOutput().lines()
808                 .toArray(String[]::new)) {
809             if (line.isBlank()) {
810                 // If line is blank and warning flag is true, it is the end of warnings section
811                 // This is needed when some info is added after warnings, such as timestamp expiration date
812                 if (warning) warning = false;
813                 continue;
814             }
815             if (Test.JAR_VERIFIED.equals(line)) continue;
816             if (line.matches(Test.ERROR + " ?") && expectedExitCode == 0) {
817                 System.out.println("verifyingStatus: error: line.matches(" + Test.ERROR + "\" ?\"): " + line);
818                 return Status.ERROR;
819             }
820             if (line.matches(Test.WARNING + " ?")) {
821                 warning = true;
822                 continue;
823             }
824             if (!warning) continue;
825             line = line.strip();
826             if (Test.NOT_YET_VALID_CERT_SIGNING_WARNING.equals(line)) continue;
827             if (Test.HAS_EXPIRING_CERT_SIGNING_WARNING.equals(line)) continue;
828             if (Test.HAS_EXPIRING_CERT_VERIFYING_WARNING.equals(line)) continue;
829             if (line.matches("^" + Test.NO_TIMESTAMP_SIGNING_WARN_TEMPLATE
830                     .replaceAll(
831                         "\\(%1\\$tY-%1\\$tm-%1\\$td\\)", "\\\\([^\\\\)]+\\\\)"
832                         + "( or after any future revocation date)?")
833                     .replaceAll("[.]", "[.]") + "$") && !tsa) continue;
834             if (line.matches("^" + Test.NO_TIMESTAMP_VERIFYING_WARN_TEMPLATE
835                     .replaceAll("\\(as early as %1\\$tY-%1\\$tm-%1\\$td\\)",
836                         "\\\\([^\\\\)]+\\\\)"
837                         + "( or after any future revocation date)?")
838                     .replaceAll("[.]", "[.]") + "$") && !tsa) continue;
839             if (line.matches("^This jar contains signatures that do(es)? not "
840                     + "include a timestamp[.] Without a timestamp, users may "
841                     + "not be able to validate this jar after the signer "
842                     + "certificate's expiration date \\([^\\)]+\\) or after "
843                     + "any future revocation date[.]") && !tsa) continue;
844 
845             if (isWeakAlg(signItem.expectedDigestAlg())
846                     && line.contains(Test.WEAK_ALGORITHM_WARNING)) continue;
847             if (Test.CERTIFICATE_SELF_SIGNED.equals(line)) continue;
848             if (Test.HAS_EXPIRED_CERT_VERIFYING_WARNING.equals(line)
849                     && signItem.certInfo.expired) continue;
850             System.out.println("verifyingStatus: unexpected line: " + line);
851             return Status.ERROR; // treat unexpected warnings as error
852         }
853         return warning ? Status.WARNING : Status.NORMAL;
854     }
855 
isWeakAlg(String alg)856     private static boolean isWeakAlg(String alg) {
857         return SHA1.equals(alg);
858     }
859 
860     // Using specified jarsigner to sign the pre-created jar with specified
861     // algorithms.
signJar(String jarsignerPath, String sigalg, String jarDigestAlgorithm, String tsadigestalg, String tsa, String alias, String unsignedJar, String signedJar)862     private static OutputAnalyzer signJar(String jarsignerPath, String sigalg,
863             String jarDigestAlgorithm,
864             String tsadigestalg, String tsa, String alias, String unsignedJar,
865             String signedJar) throws Throwable {
866         List<String> arguments = new ArrayList<>();
867 
868         if (PROXY_HOST != null && PROXY_PORT != null) {
869             arguments.add("-J-Dhttp.proxyHost=" + PROXY_HOST);
870             arguments.add("-J-Dhttp.proxyPort=" + PROXY_PORT);
871             arguments.add("-J-Dhttps.proxyHost=" + PROXY_HOST);
872             arguments.add("-J-Dhttps.proxyPort=" + PROXY_PORT);
873         }
874         arguments.add("-J-Djava.security.properties=" + JAVA_SECURITY);
875         arguments.add("-debug");
876         arguments.add("-verbose");
877         if (jarDigestAlgorithm != null) {
878             arguments.add("-digestalg");
879             arguments.add(jarDigestAlgorithm);
880         }
881         if (sigalg != null) {
882             arguments.add("-sigalg");
883             arguments.add(sigalg);
884         }
885         if (tsa != null) {
886             arguments.add("-tsa");
887             arguments.add(tsa);
888         }
889         if (tsadigestalg != null) {
890             arguments.add("-tsadigestalg");
891             arguments.add(tsadigestalg);
892         }
893         arguments.add("-keystore");
894         arguments.add(KEYSTORE);
895         arguments.add("-storepass");
896         arguments.add(PASSWORD);
897         arguments.add("-sigfile");
898         arguments.add(nextSigfileName(alias, unsignedJar, signedJar));
899         arguments.add("-signedjar");
900         arguments.add(signedJar + ".jar");
901         arguments.add(unsignedJar + ".jar");
902         arguments.add(alias);
903 
904         OutputAnalyzer outputAnalyzer = execTool(jarsignerPath,
905                 arguments.toArray(new String[arguments.size()]));
906         return outputAnalyzer;
907     }
908 
909     // Using specified jarsigner to verify the signed jar.
verifyJar(String jarsignerPath, String signedJar, String alias)910     private static OutputAnalyzer verifyJar(String jarsignerPath,
911             String signedJar, String alias) throws Throwable {
912         List<String> arguments = new ArrayList<>();
913         arguments.add("-J-Djava.security.properties=" + JAVA_SECURITY);
914         arguments.add("-debug");
915         arguments.add("-verbose");
916         arguments.add("-certs");
917         arguments.add("-keystore");
918         arguments.add(KEYSTORE);
919         arguments.add("-verify");
920         if (STRICT) arguments.add("-strict");
921         arguments.add(signedJar + ".jar");
922         if (alias != null) arguments.add(alias);
923         OutputAnalyzer outputAnalyzer = execTool(jarsignerPath,
924                 arguments.toArray(new String[arguments.size()]));
925         return outputAnalyzer;
926     }
927 
928     // Generates the test result report.
generateReport(List<JdkInfo> jdkList, List<TsaInfo> tsaList, List<SignItem> signItems)929     private static boolean generateReport(List<JdkInfo> jdkList, List<TsaInfo> tsaList,
930             List<SignItem> signItems) throws IOException {
931         System.out.println("Report is being generated...");
932 
933         StringBuilder report = new StringBuilder();
934         report.append(HtmlHelper.startHtml());
935         report.append(HtmlHelper.startPre());
936 
937         // Generates JDK list
938         report.append("JDK list:\n");
939         for(JdkInfo jdkInfo : jdkList) {
940             report.append(String.format("%d=%s%n",
941                     jdkInfo.index,
942                     jdkInfo.runtimeVersion));
943         }
944 
945         // Generates TSA URLs
946         report.append("TSA list:\n");
947         for(TsaInfo tsaInfo : tsaList) {
948             report.append(
949                     String.format("%d=%s%n", tsaInfo.index,
950                             tsaInfo.tsaUrl == null ? "notsa" : tsaInfo.tsaUrl));
951         }
952         report.append(HtmlHelper.endPre());
953 
954         report.append(HtmlHelper.startTable());
955         // Generates report headers.
956         List<String> headers = new ArrayList<>();
957         headers.add("[Jarfile]");
958         headers.add("[Signing Certificate]");
959         headers.add("[Signer JDK]");
960         headers.add("[Signature Algorithm]");
961         headers.add("[Jar Digest Algorithm]");
962         headers.add("[TSA Digest Algorithm]");
963         headers.add("[TSA]");
964         headers.add("[Signing Status]");
965         headers.add("[Verifier JDK]");
966         headers.add("[Verifying Certificate]");
967         headers.add("[Verifying Status]");
968         if (DELAY_VERIFY) {
969             headers.add("[Delay Verifying Status]");
970         }
971         headers.add("[Failed]");
972         report.append(HtmlHelper.htmlRow(
973                 headers.toArray(new String[headers.size()])));
974 
975         StringBuilder failedReport = new StringBuilder(report.toString());
976 
977         boolean failed = signItems.isEmpty();
978 
979         // Generates report rows.
980         for (SignItem signItem : signItems) {
981             failed = failed || signItem.verifyItems.isEmpty();
982             for (VerifyItem verifyItem : signItem.verifyItems) {
983                 String reportRow = reportRow(signItem, verifyItem);
984                 report.append(reportRow);
985                 boolean isFailedCase = isFailed(signItem, verifyItem);
986                 if (isFailedCase) {
987                     failedReport.append(reportRow);
988                 }
989                 failed = failed || isFailedCase;
990             }
991         }
992 
993         report.append(HtmlHelper.endTable());
994         report.append(HtmlHelper.endHtml());
995         generateFile("report.html", report.toString());
996         if (failed) {
997             failedReport.append(HtmlHelper.endTable());
998             failedReport.append(HtmlHelper.endPre());
999             failedReport.append(HtmlHelper.endHtml());
1000             generateFile("failedReport.html", failedReport.toString());
1001         }
1002 
1003         System.out.println("Report is generated.");
1004         return failed;
1005     }
1006 
generateFile(String path, String content)1007     private static void generateFile(String path, String content)
1008             throws IOException {
1009         FileWriter writer = new FileWriter(new File(path));
1010         writer.write(content);
1011         writer.close();
1012     }
1013 
jarsignerPath(String jdkPath)1014     private static String jarsignerPath(String jdkPath) {
1015         return jdkPath + "/bin/jarsigner";
1016     }
1017 
1018     // Executes the specified function on JdkUtils by the specified JDK.
execJdkUtils(String jdkPath, String method, String... args)1019     private static String execJdkUtils(String jdkPath, String method,
1020             String... args) throws Throwable {
1021         String[] cmd = new String[args.length + 5];
1022         cmd[0] = jdkPath + "/bin/java";
1023         cmd[1] = "-cp";
1024         cmd[2] = TEST_CLASSES;
1025         cmd[3] = JdkUtils.class.getName();
1026         cmd[4] = method;
1027         System.arraycopy(args, 0, cmd, 5, args.length);
1028         return ProcessTools.executeCommand(cmd).getOutput();
1029     }
1030 
1031     // Executes the specified JDK tools, such as keytool and jarsigner, and
1032     // ensures the output is in US English.
execTool(String toolPath, String... args)1033     private static OutputAnalyzer execTool(String toolPath, String... args)
1034             throws Throwable {
1035         long start = System.currentTimeMillis();
1036         try {
1037 
1038             String[] cmd = new String[args.length + 4];
1039             cmd[0] = toolPath;
1040             cmd[1] = "-J-Duser.language=en";
1041             cmd[2] = "-J-Duser.country=US";
1042             cmd[3] = "-J-Djava.security.egd=file:/dev/./urandom";
1043             System.arraycopy(args, 0, cmd, 4, args.length);
1044             return ProcessTools.executeCommand(cmd);
1045 
1046         } finally {
1047             long end = System.currentTimeMillis();
1048             System.out.println("child process duration [ms]: " + (end - start));
1049         }
1050     }
1051 
1052     private static class JdkInfo {
1053 
1054         private int index;
1055         private final String jdkPath;
1056         private final String jarsignerPath;
1057         private final String runtimeVersion;
1058         private String version;
1059         private final int majorVersion;
1060         private final boolean supportsTsadigestalg;
1061 
1062         private Map<String, Boolean> sigalgMap = new HashMap<>();
1063 
JdkInfo(String jdkPath)1064         private JdkInfo(String jdkPath) throws Throwable {
1065             this.jdkPath = jdkPath;
1066             jarsignerPath = jarsignerPath(jdkPath);
1067             runtimeVersion = execJdkUtils(jdkPath, JdkUtils.M_JAVA_RUNTIME_VERSION);
1068             if (runtimeVersion == null || runtimeVersion.isBlank()) {
1069                 throw new RuntimeException(
1070                         "Cannot determine the JDK version: " + jdkPath);
1071             }
1072             version = execJdkUtils(jdkPath, JdkUtils.M_JAVA_VERSION);
1073             majorVersion = Integer.parseInt((runtimeVersion.matches("^1[.].*") ?
1074                     runtimeVersion.substring(2) : runtimeVersion).replaceAll("[^0-9].*$", ""));
1075             supportsTsadigestalg = execTool(jarsignerPath, "-help")
1076                     .getOutput().contains("-tsadigestalg");
1077         }
1078 
isSupportedSigalg(String sigalg)1079         private boolean isSupportedSigalg(String sigalg) throws Throwable {
1080             if (!sigalgMap.containsKey(sigalg)) {
1081                 boolean isSupported = Boolean.parseBoolean(
1082                         execJdkUtils(
1083                                 jdkPath,
1084                                 JdkUtils.M_IS_SUPPORTED_SIGALG,
1085                                 sigalg));
1086                 sigalgMap.put(sigalg, isSupported);
1087             }
1088 
1089             return sigalgMap.get(sigalg);
1090         }
1091 
isAtLeastMajorVersion(int minVersion)1092         private boolean isAtLeastMajorVersion(int minVersion) {
1093             return majorVersion >= minVersion;
1094         }
1095 
supportsKeyAlg(String keyAlgorithm)1096         private boolean supportsKeyAlg(String keyAlgorithm) {
1097             // JDK 6 doesn't support EC
1098             return isAtLeastMajorVersion(6) || !EC.equals(keyAlgorithm);
1099         }
1100 
1101         @Override
hashCode()1102         public int hashCode() {
1103             final int prime = 31;
1104             int result = 1;
1105             result = prime * result
1106                     + ((runtimeVersion == null) ? 0 : runtimeVersion.hashCode());
1107             return result;
1108         }
1109 
1110         @Override
equals(Object obj)1111         public boolean equals(Object obj) {
1112             if (this == obj)
1113                 return true;
1114             if (obj == null)
1115                 return false;
1116             if (getClass() != obj.getClass())
1117                 return false;
1118             JdkInfo other = (JdkInfo) obj;
1119             if (runtimeVersion == null) {
1120                 if (other.runtimeVersion != null)
1121                     return false;
1122             } else if (!runtimeVersion.equals(other.runtimeVersion))
1123                 return false;
1124             return true;
1125         }
1126 
1127         @Override
toString()1128         public String toString() {
1129             return "JdkInfo[" + runtimeVersion + ", " + jdkPath + "]";
1130         }
1131     }
1132 
1133     private static class TsaInfo {
1134 
1135         private final int index;
1136         private final String tsaUrl;
1137         private Set<String> digestList = new HashSet<>();
1138 
TsaInfo(int index, String tsa)1139         private TsaInfo(int index, String tsa) {
1140             this.index = index;
1141             this.tsaUrl = tsa;
1142         }
1143 
addDigest(String digest)1144         private void addDigest(String digest) {
1145             digestList.add(digest);
1146         }
1147 
isDigestSupported(String digest)1148         private boolean isDigestSupported(String digest) {
1149             return digest == null || digestList.isEmpty()
1150                     || digestList.contains(digest);
1151         }
1152 
1153         @Override
toString()1154         public String toString() {
1155             return "TsaInfo[" + index + ", " + tsaUrl + "]";
1156         }
1157     }
1158 
1159     private static class CertInfo {
1160 
1161         private static int certCounter;
1162 
1163         // nr distinguishes cert CNs in jarsigner -verify output
1164         private final int nr = ++certCounter;
1165         private final JdkInfo jdkInfo;
1166         private final String keyAlgorithm;
1167         private final String digestAlgorithm;
1168         private final int keySize;
1169         private final boolean expired;
1170 
CertInfo(JdkInfo jdkInfo, String keyAlgorithm, String digestAlgorithm, int keySize, boolean expired)1171         private CertInfo(JdkInfo jdkInfo, String keyAlgorithm,
1172                 String digestAlgorithm, int keySize, boolean expired) {
1173             this.jdkInfo = jdkInfo;
1174             this.keyAlgorithm = keyAlgorithm;
1175             this.digestAlgorithm = digestAlgorithm;
1176             this.keySize = keySize;
1177             this.expired = expired;
1178         }
1179 
sigalg()1180         private String sigalg() {
1181             return DEFAULT.equals(digestAlgorithm) ? null : expectedSigalg();
1182         }
1183 
expectedSigalg()1184         private String expectedSigalg() {
1185             return (DEFAULT.equals(this.digestAlgorithm) ? this.digestAlgorithm
1186                     : "SHA-256").replace("-", "") + "with" +
1187                     keyAlgorithm + (EC.equals(keyAlgorithm) ? "DSA" : "");
1188         }
1189 
expectedKeySize()1190         private int expectedKeySize() {
1191             if (keySize != 0) return keySize;
1192 
1193             // defaults
1194             if (RSA.equals(keyAlgorithm) || DSA.equals(keyAlgorithm)) {
1195                 return 2048;
1196             } else if (EC.equals(keyAlgorithm)) {
1197                 return 256;
1198             } else {
1199                 throw new RuntimeException("problem determining key size");
1200             }
1201         }
1202 
1203         @Override
hashCode()1204         public int hashCode() {
1205             final int prime = 31;
1206             int result = 1;
1207             result = prime * result
1208                     + (digestAlgorithm == null ? 0 : digestAlgorithm.hashCode());
1209             result = prime * result + (expired ? 1231 : 1237);
1210             result = prime * result
1211                     + (jdkInfo == null ? 0 : jdkInfo.hashCode());
1212             result = prime * result
1213                     + (keyAlgorithm == null ? 0 : keyAlgorithm.hashCode());
1214             result = prime * result + keySize;
1215             return result;
1216         }
1217 
1218         @Override
equals(Object obj)1219         public boolean equals(Object obj) {
1220             if (this == obj)
1221                 return true;
1222             if (obj == null)
1223                 return false;
1224             if (getClass() != obj.getClass())
1225                 return false;
1226             CertInfo other = (CertInfo) obj;
1227             if (digestAlgorithm == null) {
1228                 if (other.digestAlgorithm != null)
1229                     return false;
1230             } else if (!digestAlgorithm.equals(other.digestAlgorithm))
1231                 return false;
1232             if (expired != other.expired)
1233                 return false;
1234             if (jdkInfo == null) {
1235                 if (other.jdkInfo != null)
1236                     return false;
1237             } else if (!jdkInfo.equals(other.jdkInfo))
1238                 return false;
1239             if (keyAlgorithm == null) {
1240                 if (other.keyAlgorithm != null)
1241                     return false;
1242             } else if (!keyAlgorithm.equals(other.keyAlgorithm))
1243                 return false;
1244             if (keySize != other.keySize)
1245                 return false;
1246             return true;
1247         }
1248 
alias()1249         private String alias() {
1250             return (jdkInfo.version + "_" + toString())
1251                     // lower case for jks due to
1252                     // sun.security.provider.JavaKeyStore.JDK.convertAlias
1253                     .toLowerCase(Locale.ENGLISH);
1254         }
1255 
1256         @Override
toString()1257         public String toString() {
1258             return "nr" + nr + "_"
1259                     + keyAlgorithm + "_" + digestAlgorithm
1260                     + (keySize == 0 ? "" : "_" + keySize)
1261                     + (expired ? "_Expired" : "");
1262         }
1263     }
1264 
1265     // It does only one timestamping for the same JDK, digest algorithm and
1266     // TSA service with an arbitrary valid/expired certificate.
1267     private static class TsaFilter {
1268 
1269         private static final Set<Condition> SET = new HashSet<>();
1270 
filter(String signerVersion, String digestAlgorithm, boolean expiredCert, int tsaIndex)1271         private static boolean filter(String signerVersion,
1272                 String digestAlgorithm, boolean expiredCert, int tsaIndex) {
1273             return !SET.add(new Condition(signerVersion, digestAlgorithm,
1274                     expiredCert, tsaIndex));
1275         }
1276 
1277         private static class Condition {
1278 
1279             private final String signerVersion;
1280             private final String digestAlgorithm;
1281             private final boolean expiredCert;
1282             private final int tsaIndex;
1283 
Condition(String signerVersion, String digestAlgorithm, boolean expiredCert, int tsaIndex)1284             private Condition(String signerVersion, String digestAlgorithm,
1285                     boolean expiredCert, int tsaIndex) {
1286                 this.signerVersion = signerVersion;
1287                 this.digestAlgorithm = digestAlgorithm;
1288                 this.expiredCert = expiredCert;
1289                 this.tsaIndex = tsaIndex;
1290             }
1291 
1292             @Override
hashCode()1293             public int hashCode() {
1294                 final int prime = 31;
1295                 int result = 1;
1296                 result = prime * result
1297                         + ((digestAlgorithm == null) ? 0 : digestAlgorithm.hashCode());
1298                 result = prime * result + (expiredCert ? 1231 : 1237);
1299                 result = prime * result
1300                         + ((signerVersion == null) ? 0 : signerVersion.hashCode());
1301                 result = prime * result + tsaIndex;
1302                 return result;
1303             }
1304 
1305             @Override
equals(Object obj)1306             public boolean equals(Object obj) {
1307                 if (this == obj)
1308                     return true;
1309                 if (obj == null)
1310                     return false;
1311                 if (getClass() != obj.getClass())
1312                     return false;
1313                 Condition other = (Condition) obj;
1314                 if (digestAlgorithm == null) {
1315                     if (other.digestAlgorithm != null)
1316                         return false;
1317                 } else if (!digestAlgorithm.equals(other.digestAlgorithm))
1318                     return false;
1319                 if (expiredCert != other.expiredCert)
1320                     return false;
1321                 if (signerVersion == null) {
1322                     if (other.signerVersion != null)
1323                         return false;
1324                 } else if (!signerVersion.equals(other.signerVersion))
1325                     return false;
1326                 if (tsaIndex != other.tsaIndex)
1327                     return false;
1328                 return true;
1329             }
1330         }}
1331 
1332     private static enum Status {
1333 
1334         // No action due to pre-action fails.
1335         NONE,
1336 
1337         // jar is signed/verified with error
1338         ERROR,
1339 
1340         // jar is signed/verified with warning
1341         WARNING,
1342 
1343         // jar is signed/verified without any warning and error
1344         NORMAL
1345     }
1346 
1347     private static class SignItem {
1348 
1349         private SignItem prevSign;
1350         private CertInfo certInfo;
1351         private JdkInfo jdkInfo;
1352         private String digestAlgorithm;
1353         private String tsaDigestAlgorithm;
1354         private int tsaIndex;
1355         private Status status;
1356         private String unsignedJar;
1357         private String signedJar;
1358         private List<String> jarContents = new ArrayList<>();
1359 
1360         private List<VerifyItem> verifyItems = new ArrayList<>();
1361 
build()1362         private static SignItem build() {
1363             return new SignItem()
1364                     .addContentFiles(Arrays.asList("META-INF/MANIFEST.MF"));
1365         }
1366 
build(SignItem prevSign)1367         private static SignItem build(SignItem prevSign) {
1368             return build().prevSign(prevSign).unsignedJar(prevSign.signedJar)
1369                     .addContentFiles(prevSign.jarContents);
1370         }
1371 
prevSign(SignItem prevSign)1372         private SignItem prevSign(SignItem prevSign) {
1373             this.prevSign = prevSign;
1374             return this;
1375         }
1376 
certInfo(CertInfo certInfo)1377         private SignItem certInfo(CertInfo certInfo) {
1378             this.certInfo = certInfo;
1379             return this;
1380         }
1381 
jdkInfo(JdkInfo jdkInfo)1382         private SignItem jdkInfo(JdkInfo jdkInfo) {
1383             this.jdkInfo = jdkInfo;
1384             return this;
1385         }
1386 
digestAlgorithm(String digestAlgorithm)1387         private SignItem digestAlgorithm(String digestAlgorithm) {
1388             this.digestAlgorithm = digestAlgorithm;
1389             return this;
1390         }
1391 
expectedDigestAlg()1392         String expectedDigestAlg() {
1393             return digestAlgorithm != null ? digestAlgorithm : "SHA-256";
1394         }
1395 
tsaDigestAlgorithm(String tsaDigestAlgorithm)1396         private SignItem tsaDigestAlgorithm(String tsaDigestAlgorithm) {
1397             this.tsaDigestAlgorithm = tsaDigestAlgorithm;
1398             return this;
1399         }
1400 
expectedTsaDigestAlg()1401         String expectedTsaDigestAlg() {
1402             return tsaDigestAlgorithm != null ? tsaDigestAlgorithm : "SHA-256";
1403         }
1404 
tsaIndex(int tsaIndex)1405         private SignItem tsaIndex(int tsaIndex) {
1406             this.tsaIndex = tsaIndex;
1407             return this;
1408         }
1409 
status(Status status)1410         private SignItem status(Status status) {
1411             this.status = status;
1412             return this;
1413         }
1414 
unsignedJar(String unsignedJar)1415         private SignItem unsignedJar(String unsignedJar) {
1416             this.unsignedJar = unsignedJar;
1417             return this;
1418         }
1419 
signedJar(String signedJar)1420         private SignItem signedJar(String signedJar) {
1421             this.signedJar = signedJar;
1422             return this;
1423         }
1424 
addContentFiles(List<String> files)1425         private SignItem addContentFiles(List<String> files) {
1426             this.jarContents.addAll(files);
1427             return this;
1428         }
1429 
addVerifyItem(VerifyItem verifyItem)1430         private void addVerifyItem(VerifyItem verifyItem) {
1431             verifyItems.add(verifyItem);
1432         }
1433 
isErrorInclPrev()1434         private boolean isErrorInclPrev() {
1435             if (prevSign != null && prevSign.isErrorInclPrev()) {
1436                 System.out.println("SignItem.isErrorInclPrev: returning true from previous");
1437                 return true;
1438             }
1439 
1440             return status == Status.ERROR;
1441         }
toStringWithPrev(Function<SignItem,String> toStr)1442         private List<String> toStringWithPrev(Function<SignItem,String> toStr) {
1443             List<String> s = new ArrayList<>();
1444             if (prevSign != null) {
1445                 s.addAll(prevSign.toStringWithPrev(toStr));
1446             }
1447             if (status != null) { // no status means jar creation or update item
1448                 s.add(toStr.apply(this));
1449             }
1450             return s;
1451         }
1452     }
1453 
1454     private static class VerifyItem {
1455 
1456         private VerifyItem prevVerify;
1457         private CertInfo certInfo;
1458         private JdkInfo jdkInfo;
1459         private Status status = Status.NONE;
1460         private Status delayStatus = Status.NONE;
1461 
build(JdkInfo jdkInfo)1462         private static VerifyItem build(JdkInfo jdkInfo) {
1463             VerifyItem verifyItem = new VerifyItem();
1464             verifyItem.jdkInfo = jdkInfo;
1465             return verifyItem;
1466         }
1467 
certInfo(CertInfo certInfo)1468         private VerifyItem certInfo(CertInfo certInfo) {
1469             this.certInfo = certInfo;
1470             return this;
1471         }
1472 
addSignerCertInfos(SignItem signItem)1473         private void addSignerCertInfos(SignItem signItem) {
1474             VerifyItem prevVerify = this;
1475             CertInfo lastCertInfo = null;
1476             while (signItem != null) {
1477                 // (signItem.certInfo == null) means create or update jar step
1478                 if (signItem.certInfo != null
1479                         && !signItem.certInfo.equals(lastCertInfo)) {
1480                     lastCertInfo = signItem.certInfo;
1481                     prevVerify = prevVerify.prevVerify =
1482                             build(jdkInfo).certInfo(signItem.certInfo);
1483                 }
1484                 signItem = signItem.prevSign;
1485             }
1486         }
1487 
status(Status status)1488         private VerifyItem status(Status status) {
1489             this.status = status;
1490             return this;
1491         }
1492 
isErrorInclPrev()1493         private boolean isErrorInclPrev() {
1494             if (prevVerify != null && prevVerify.isErrorInclPrev()) {
1495                 System.out.println("VerifyItem.isErrorInclPrev: returning true from previous");
1496                 return true;
1497             }
1498 
1499             return status == Status.ERROR || delayStatus == Status.ERROR;
1500         }
1501 
delayStatus(Status status)1502         private VerifyItem delayStatus(Status status) {
1503             this.delayStatus = status;
1504             return this;
1505         }
1506 
toStringWithPrev( Function<VerifyItem,String> toStr)1507         private List<String> toStringWithPrev(
1508                 Function<VerifyItem,String> toStr) {
1509             List<String> s = new ArrayList<>();
1510             if (prevVerify != null) {
1511                 s.addAll(prevVerify.toStringWithPrev(toStr));
1512             }
1513             s.add(toStr.apply(this));
1514             return s;
1515         }
1516     }
1517 
1518     // The identifier for a specific signing.
signingId(SignItem signItem)1519     private static String signingId(SignItem signItem) {
1520         return signItem.signedJar;
1521     }
1522 
1523     // The identifier for a specific verifying.
verifyingId(SignItem signItem, VerifyItem verifyItem, boolean delayVerify)1524     private static String verifyingId(SignItem signItem, VerifyItem verifyItem,
1525             boolean delayVerify) {
1526         return signingId(signItem) + (delayVerify ? "-DV" : "-V")
1527                 + "_" + verifyItem.jdkInfo.version +
1528                 (verifyItem.certInfo == null ? "" : "_" + verifyItem.certInfo);
1529     }
1530 
reportRow(SignItem signItem, VerifyItem verifyItem)1531     private static String reportRow(SignItem signItem, VerifyItem verifyItem) {
1532         List<String> values = new ArrayList<>();
1533         Consumer<Function<SignItem, String>> s_values_add = f -> {
1534             values.add(String.join("<br/><br/>", signItem.toStringWithPrev(f)));
1535         };
1536         Consumer<Function<VerifyItem, String>> v_values_add = f -> {
1537             values.add(String.join("<br/><br/>", verifyItem.toStringWithPrev(f)));
1538         };
1539         s_values_add.accept(i -> i.unsignedJar + " -> " + i.signedJar);
1540         s_values_add.accept(i -> i.certInfo.toString());
1541         s_values_add.accept(i -> i.jdkInfo.version);
1542         s_values_add.accept(i -> i.certInfo.expectedSigalg());
1543         s_values_add.accept(i ->
1544                 null2Default(i.digestAlgorithm, i.expectedDigestAlg()));
1545         s_values_add.accept(i -> i.tsaIndex == -1 ? "" :
1546                 null2Default(i.tsaDigestAlgorithm, i.expectedTsaDigestAlg()));
1547         s_values_add.accept(i -> i.tsaIndex == -1 ? "" : i.tsaIndex + "");
1548         s_values_add.accept(i -> HtmlHelper.anchorLink(
1549                 PhaseOutputStream.fileName(PhaseOutputStream.Phase.SIGNING),
1550                 signingId(i),
1551                 "" + i.status));
1552         values.add(verifyItem.jdkInfo.version);
1553         v_values_add.accept(i ->
1554                 i.certInfo == null ? "no alias" : "" + i.certInfo);
1555         v_values_add.accept(i -> HtmlHelper.anchorLink(
1556                 PhaseOutputStream.fileName(PhaseOutputStream.Phase.VERIFYING),
1557                 verifyingId(signItem, i, false),
1558                 "" + i.status.toString()));
1559         if (DELAY_VERIFY) {
1560             v_values_add.accept(i -> HtmlHelper.anchorLink(
1561                     PhaseOutputStream.fileName(
1562                             PhaseOutputStream.Phase.DELAY_VERIFYING),
1563                     verifyingId(signItem, verifyItem, true),
1564                     verifyItem.delayStatus.toString()));
1565         }
1566         values.add(isFailed(signItem, verifyItem) ? "X" : "");
1567         return HtmlHelper.htmlRow(values.toArray(new String[values.size()]));
1568     }
1569 
isFailed(SignItem signItem, VerifyItem verifyItem)1570     private static boolean isFailed(SignItem signItem, VerifyItem verifyItem) {
1571         System.out.println("isFailed: signItem = " + signItem + ", verifyItem = " + verifyItem);
1572         // TODO: except known failing cases
1573 
1574         // Note about isAtLeastMajorVersion in the following conditions:
1575         // signItem.jdkInfo is the jdk which signed the jar last and
1576         // signItem.prevSign.jdkInfo is the jdk which signed the jar first
1577         // assuming only two successive signatures as there actually are now.
1578         // the first signature always works and always has. subject here is
1579         // the update of an already signed jar. the following conditions always
1580         // depend on the second jdk that updated the jar with another signature
1581         // and the first one (signItem(.prevSign)+.jdkInfo) can be ignored.
1582         // this is different for verifyItem. verifyItem.prevVerify refers to
1583         // the first signature created by signItem(.prevSign)+.jdkInfo.
1584         // all verifyItem(.prevVerify)+.jdkInfo however point always to the same
1585         // jdk, only their certInfo is different. the same signatures are
1586         // verified with different jdks in different top-level VerifyItems
1587         // attached directly to signItem.verifyItems and not to
1588         // verifyItem.prevVerify.
1589 
1590         // ManifestDigester fails to parse manifests ending in '\r' with
1591         // IndexOutOfBoundsException at ManifestDigester.java:87 before 8217375
1592         if (signItem.signedJar.startsWith("eofr")
1593                 && !signItem.jdkInfo.isAtLeastMajorVersion(13)
1594                 && !verifyItem.jdkInfo.isAtLeastMajorVersion(13)) return false;
1595 
1596         // if there is no blank line after main attributes, JarSigner adds
1597         // individual sections nevertheless without being properly delimited
1598         // in JarSigner.java:777..790 without checking for blank line
1599         // before 8217375
1600 //        if (signItem.signedJar.startsWith("eofn-")
1601 //                && signItem.signedJar.contains("-addfile-")
1602 //                && !signItem.jdkInfo.isAtLeastMajorVersion(13)
1603 //                && !verifyItem.jdkInfo.isAtLeastMajorVersion(13)) return false; // FIXME
1604 
1605 //        System.out.println("isFailed: signItem.isErrorInclPrev() " + signItem.isErrorInclPrev());
1606 //        System.out.println("isFailed: verifyItem.isErrorInclPrev() " + verifyItem.isErrorInclPrev());
1607         boolean isFailed = signItem.isErrorInclPrev() || verifyItem.isErrorInclPrev();
1608         System.out.println("isFailed: returning " + isFailed);
1609         return isFailed;
1610     }
1611 
1612     // If a value is null, then displays the default value or N/A.
null2Default(String value, String defaultValue)1613     private static String null2Default(String value, String defaultValue) {
1614         return value != null ? value :
1615                DEFAULT + "(" + (defaultValue == null
1616                                   ? "N/A"
1617                                   : defaultValue) + ")";
1618     }
1619 
1620 }
1621