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