1 /*
   2  * Copyright (c) 2014, 2016, 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.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.oracle.tools.packager.mac;
  27 
  28 import com.oracle.tools.packager.AbstractBundler;
  29 import com.oracle.tools.packager.Bundler;
  30 import com.oracle.tools.packager.BundlerParamInfo;
  31 import com.oracle.tools.packager.ConfigException;
  32 import com.oracle.tools.packager.IOUtils;
  33 import com.oracle.tools.packager.Log;
  34 import com.oracle.tools.packager.RelativeFileSet;
  35 import com.oracle.tools.packager.UnsupportedPlatformException;
  36 import org.junit.After;
  37 import org.junit.Assume;
  38 import org.junit.Before;
  39 import org.junit.BeforeClass;
  40 import org.junit.Ignore;
  41 import org.junit.Test;
  42 
  43 import java.io.ByteArrayOutputStream;
  44 import java.io.File;
  45 import java.io.IOException;
  46 import java.io.PrintStream;
  47 import java.nio.file.Files;
  48 import java.util.Arrays;
  49 import java.util.Collection;
  50 import java.util.HashMap;
  51 import java.util.HashSet;
  52 import java.util.Map;
  53 import java.util.Set;
  54 import java.util.TreeMap;
  55 
  56 import static com.oracle.tools.packager.StandardBundlerParam.*;
  57 import static com.oracle.tools.packager.mac.MacAppBundler.*;
  58 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.MAC_APP_IMAGE;
  59 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEYCHAIN;
  60 import static com.oracle.tools.packager.mac.MacPkgBundler.DEVELOPER_ID_INSTALLER_SIGNING_KEY;
  61 import static com.oracle.tools.packager.mac.MacPkgBundler.INSTALLER_SUFFIX;
  62 import static org.junit.Assert.*;
  63 
  64 public class MacPkgBundlerTest {
  65 
  66     static final int MIN_SIZE=0x100000; // 1MiB
  67 
  68     static File tmpBase;
  69     static File workDir;
  70     static File appResourcesDir;
  71     static File fakeMainJar;
  72     static File hdpiIcon;
  73     static String runtimeJdk;
  74     static Set<File> appResources;
  75     static boolean retain = false;
  76     static boolean signingKeysPresent = false;
  77 
  78     static final File FAKE_CERT_ROOT = new File("build/tmp/tests/cert/").getAbsoluteFile();
  79 
  80     @BeforeClass
  81     public static void prepareApp() {
  82         // only run on mac
  83         Assume.assumeTrue(System.getProperty("os.name").toLowerCase().contains("os x"));
  84 
  85         runtimeJdk = System.getenv("PACKAGER_JDK_ROOT");
  86 
  87         // and only if we have the correct JRE settings
  88         String jre = System.getProperty("java.home").toLowerCase();
  89         Assume.assumeTrue(runtimeJdk != null || jre.endsWith("/contents/home/jre") || jre.endsWith("/contents/home/jre"));
  90 
  91         Log.setLogger(new Log.Logger(true));
  92         Log.setDebug(true);
  93 
  94         retain = Boolean.parseBoolean(System.getProperty("RETAIN_PACKAGER_TESTS"));
  95 
  96         workDir = new File("build/tmp/tests", "macpkg");
  97         hdpiIcon = new File("build/tmp/tests", "GenericAppHiDPI.icns");
  98         appResourcesDir = new File("build/tmp/tests", "appResources");
  99         fakeMainJar = new File(appResourcesDir, "mainApp.jar");
 100 
 101         appResources = new HashSet<>(Arrays.asList(fakeMainJar,
 102                 new File(appResourcesDir, "LICENSE"),
 103                 new File(appResourcesDir, "LICENSE2")
 104         ));
 105 
 106         signingKeysPresent = DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(new TreeMap<>()) != null;
 107     }
 108 
 109     @Before
 110     public void createTmpDir() throws IOException {
 111         if (retain) {
 112             tmpBase = new File("build/tmp/tests/macpkg");
 113         } else {
 114             tmpBase = BUILD_ROOT.fetchFrom(new TreeMap<>());
 115         }
 116         tmpBase.mkdir();
 117     }
 118 
 119     public String createFakeCerts(Map<String, ? super Object> p) {
 120         File config = new File(FAKE_CERT_ROOT, "pkg-cert.cfg");
 121         config.getParentFile().mkdirs();
 122         try {
 123             // create the config file holding the key config
 124             Files.write(config.toPath(), Arrays.<String>asList("[ codesign ]",
 125                     "keyUsage=critical,digitalSignature",
 126                     "basicConstraints=critical,CA:false",
 127                     "extendedKeyUsage=critical,codeSigning",
 128                     "[ productbuild ]",
 129                     "basicConstraints=critical,CA:false",
 130                     "keyUsage=critical,digitalSignature",
 131                     "extendedKeyUsage=critical,1.2.840.113635.100.4.13",
 132                     "1.2.840.113635.100.6.1.14=critical,DER:0500"));
 133 
 134             // create the SSL keys
 135             ProcessBuilder pb = new ProcessBuilder("openssl", "req",
 136                     "-newkey", "rsa:2048",
 137                     "-nodes",
 138                     "-out", FAKE_CERT_ROOT + "/pkg-app.csr",
 139                     "-keyout", FAKE_CERT_ROOT + "/pkg-app.key",
 140                     "-subj", "/CN=Developer ID Application: Insecure Test Cert/OU=JavaFX Dev/O=Oracle/C=US");
 141             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 142 
 143             // first, for the app
 144             // create the cert
 145             pb = new ProcessBuilder("openssl", "x509",
 146                     "-req",
 147                     "-days", "10",
 148                     "-in", FAKE_CERT_ROOT + "/pkg-app.csr",
 149                     "-signkey", FAKE_CERT_ROOT + "/pkg-app.key",
 150                     "-out", FAKE_CERT_ROOT + "/pkg-app.crt",
 151                     "-extfile", FAKE_CERT_ROOT + "/pkg-cert.cfg",
 152                     "-extensions", "codesign");
 153             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 154 
 155             // create and add it to the keychain
 156             pb = new ProcessBuilder("certtool",
 157                     "i", FAKE_CERT_ROOT + "/pkg-app.crt",
 158                     "k=" + FAKE_CERT_ROOT + "/pkg.keychain",
 159                     "r=" + FAKE_CERT_ROOT + "/pkg-app.key",
 160                     "c",
 161                     "p=");
 162             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 163 
 164             // create the pkg SSL keys
 165             pb = new ProcessBuilder("openssl", "req",
 166                     "-newkey", "rsa:2048",
 167                     "-nodes",
 168                     "-out", FAKE_CERT_ROOT + "/pkg-pkg.csr",
 169                     "-keyout", FAKE_CERT_ROOT + "/pkg-pkg.key",
 170                     "-subj", "/CN=Developer ID Installer: Insecure Test Cert/OU=JavaFX Dev/O=Oracle/C=US");
 171             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 172 
 173             // create the pkg cert
 174             pb = new ProcessBuilder("openssl", "x509",
 175                         "-req",
 176                         "-days", "10",
 177                         "-in", FAKE_CERT_ROOT + "/pkg-pkg.csr",
 178                         "-signkey", FAKE_CERT_ROOT + "/pkg-pkg.key",
 179                         "-out", FAKE_CERT_ROOT + "/pkg-pkg.crt",
 180                         "-extfile",FAKE_CERT_ROOT + "/pkg-cert.cfg",
 181                         "-extensions", "productbuild");
 182             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 183 
 184             // create and add it to the keychain
 185             pb = new ProcessBuilder("certtool",
 186                     "i", FAKE_CERT_ROOT + "/pkg-pkg.crt",
 187                     "k=" + FAKE_CERT_ROOT + "/pkg.keychain",
 188                     "r=" + FAKE_CERT_ROOT + "/pkg-pkg.key");
 189             IOUtils.exec(pb, VERBOSE.fetchFrom(p));
 190 
 191             return FAKE_CERT_ROOT + "/pkg.keychain";
 192         } catch (IOException e) {
 193             e.printStackTrace();
 194         }
 195 
 196         return null;
 197     }
 198 
 199     @After
 200     public void maybeCleanupTmpDir() {
 201         if (!retain) {
 202             attemptDelete(tmpBase);
 203         }
 204         attemptDelete(FAKE_CERT_ROOT);
 205     }
 206 
 207     private void attemptDelete(File tmpBase) {
 208         if (tmpBase.isDirectory()) {
 209             File[] children = tmpBase.listFiles();
 210             if (children != null) {
 211                 for (File f : children) {
 212                     attemptDelete(f);
 213                 }
 214             }
 215         }
 216         boolean success;
 217         try {
 218             success = !tmpBase.exists() || tmpBase.delete();
 219         } catch (SecurityException se) {
 220             success = false;
 221         }
 222         if (!success) {
 223             System.err.println("Could not clean up " + tmpBase.toString());
 224         }
 225     }
 226 
 227     /**
 228      * See if smoke comes out
 229      */
 230     @Test
 231     public void smokeTest() throws IOException, ConfigException, UnsupportedPlatformException {
 232         AbstractBundler bundler = new MacPkgBundler();
 233 
 234         assertNotNull(bundler.getName());
 235         assertNotNull(bundler.getID());
 236         assertNotNull(bundler.getDescription());
 237         //assertNotNull(bundler.getBundleParameters());
 238 
 239         Map<String, Object> bundleParams = new HashMap<>();
 240 
 241         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 242 
 243         bundleParams.put(APP_NAME.getID(), "Smoke Test");
 244         bundleParams.put(MAIN_CLASS.getID(), "hello.HelloRectangle");
 245         bundleParams.put(PREFERENCES_ID.getID(), "the/really/long/preferences/id");
 246         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 247         bundleParams.put(MAIN_JAR.getID(),
 248                 new RelativeFileSet(fakeMainJar.getParentFile(),
 249                         new HashSet<>(Arrays.asList(fakeMainJar)))
 250         );
 251         bundleParams.put(CLASSPATH.getID(), "mainApp.jar");
 252         bundleParams.put(VERBOSE.getID(), true);
 253         bundleParams.put(LICENSE_FILE.getID(), Arrays.asList("LICENSE", "LICENSE2"));
 254         bundleParams.put(SIGN_BUNDLE.getID(), false); // force no signing
 255 
 256         if (runtimeJdk != null) {
 257             bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 258         }
 259 
 260         boolean valid = bundler.validate(bundleParams);
 261         assertTrue(valid);
 262 
 263         File result = bundler.execute(bundleParams, new File(workDir, "smoke"));
 264         System.err.println("Bundle at - " + result);
 265         assertNotNull(result);
 266         assertTrue(result.exists());
 267         assertTrue(result.length() > MIN_SIZE);
 268     }
 269 
 270     /**
 271      * Build smoke test and mark it as quarantined, possibly signed
 272      */
 273     @Ignore("JDK-8193232")
 274     @Test
 275     public void quarantinedAppTest() throws IOException, ConfigException, UnsupportedPlatformException {
 276 
 277         AbstractBundler bundler = new MacPkgBundler();
 278 
 279         assertNotNull(bundler.getName());
 280         assertNotNull(bundler.getID());
 281         assertNotNull(bundler.getDescription());
 282         //assertNotNull(bundler.getBundleParameters());
 283 
 284         Map<String, Object> bundleParams = new HashMap<>();
 285 
 286         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 287 
 288         bundleParams.put(APP_NAME.getID(), "Quarantine App");
 289         bundleParams.put(MAIN_CLASS.getID(), "hello.HelloRectangle");
 290         bundleParams.put(PREFERENCES_ID.getID(), "the/really/long/preferences/id");
 291         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 292         bundleParams.put(VERBOSE.getID(), true);
 293 
 294         if (runtimeJdk != null) {
 295             bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 296         }
 297 
 298         if (!signingKeysPresent) {
 299             String keychain = createFakeCerts(bundleParams);
 300             Assume.assumeNotNull(keychain);
 301             bundleParams.put(SIGNING_KEYCHAIN.getID(), keychain);
 302         }
 303 
 304         boolean valid = bundler.validate(bundleParams);
 305         assertTrue(valid);
 306 
 307         File result = bundler.execute(bundleParams, new File(workDir, "quarantine"));
 308         System.err.println("Bundle at - " + result);
 309         assertNotNull(result);
 310         assertTrue(result.exists());
 311         assertTrue(result.length() > MIN_SIZE);
 312         validateSignatures(result);
 313 
 314         // mark it as though it's been downloaded
 315         ProcessBuilder pb = new ProcessBuilder(
 316                 "xattr", "-w", "com.apple.quarantine",
 317                 "0000;" + Long.toHexString(System.currentTimeMillis() / 1000L) + ";Java Unit Tests;|com.oracle.jvm.8u",
 318                 result.toString());
 319         IOUtils.exec(pb, true);
 320     }
 321 
 322     /**
 323      * The bare minimum configuration needed to make it work
 324      * <ul>
 325      *     <li>Where to build it</li>
 326      *     <li>The jar containing the application (with a main-class attribute)</li>
 327      * </ul>
 328      *
 329      * All other values will be driven off of those two values.
 330      */
 331     @Ignore("JDK-8193232")
 332     @Test
 333     public void minimumConfig() throws IOException, ConfigException, UnsupportedPlatformException {
 334         Bundler bundler = new MacPkgBundler();
 335 
 336         Map<String, Object> bundleParams = new HashMap<>();
 337 
 338         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 339 
 340         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 341 
 342         if (runtimeJdk != null) {
 343             bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 344         }
 345 
 346         String keychain = null;
 347         if (!signingKeysPresent) {
 348             keychain = createFakeCerts(bundleParams);
 349             if (keychain != null) {
 350                 bundleParams.put(SIGNING_KEYCHAIN.getID(), keychain);
 351             }
 352         }
 353 
 354         boolean valid = bundler.validate(bundleParams);
 355         assertTrue(valid);
 356 
 357         File output = bundler.execute(bundleParams, new File(workDir, "BareMinimum"));
 358         System.err.println("Bundle at - " + output);
 359         assertNotNull(output);
 360         assertTrue(output.exists());
 361         assertTrue(output.length() > MIN_SIZE);
 362         if (signingKeysPresent || keychain != null) {
 363             validateSignatures(output);
 364         }
 365     }
 366 
 367     /**
 368      * Test with unicode in places we expect it to be
 369      */
 370     @Ignore("JDK-8163859")
 371     @Test
 372     public void unicodeConfig() throws IOException, ConfigException, UnsupportedPlatformException {
 373         Bundler bundler = new MacPkgBundler();
 374 
 375         Map<String, Object> bundleParams = new HashMap<>();
 376 
 377         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 378 
 379         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 380 
 381         bundleParams.put(APP_NAME.getID(), "хелловорлд");
 382         bundleParams.put(TITLE.getID(), "ХеллоВорлд аппликейшн");
 383         bundleParams.put(VENDOR.getID(), "Оракл девелопмент");
 384         bundleParams.put(DESCRIPTION.getID(), "крайне большое описание со странными символами");
 385 
 386         if (runtimeJdk != null) {
 387             bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 388         }
 389 
 390         String keychain = null;
 391         if (!signingKeysPresent) {
 392             keychain = createFakeCerts(bundleParams);
 393             if (keychain != null) {
 394                 bundleParams.put(SIGNING_KEYCHAIN.getID(), keychain);
 395             }
 396         }
 397 
 398         bundler.validate(bundleParams);
 399 
 400         File output = bundler.execute(bundleParams, new File(workDir, "Unicode"));
 401         System.err.println("Bundle at - " + output);
 402         assertNotNull(output);
 403         assertTrue(output.exists());
 404         if (signingKeysPresent || keychain != null) {
 405             validateSignatures(output);
 406         }
 407     }
 408 
 409     /**
 410      * Create a PKG with an external app rather than a self-created one.
 411      */
 412     @Test
 413     public void externalApp() throws IOException, ConfigException, UnsupportedPlatformException {
 414         // only run with full tests
 415         Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("FULL_TEST")));
 416 
 417         // first create the external app
 418         Bundler appBundler = new MacAppBundler();
 419 
 420         Map<String, Object> appBundleParams = new HashMap<>();
 421 
 422         appBundleParams.put(BUILD_ROOT.getID(), tmpBase);
 423 
 424         appBundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 425         appBundleParams.put(APP_NAME.getID(), "External APP PKG Test");
 426         appBundleParams.put(IDENTIFIER.getID(), "com.example.pkg.external");
 427         appBundleParams.put(VERBOSE.getID(), true);
 428 
 429         if (runtimeJdk != null) {
 430             appBundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 431         }
 432 
 433         String keychain = null;
 434         if (!signingKeysPresent) {
 435             keychain = createFakeCerts(appBundleParams);
 436             if (keychain != null) {
 437                 appBundleParams.put(SIGNING_KEYCHAIN.getID(), keychain);
 438             }
 439         }
 440 
 441         boolean valid = appBundler.validate(appBundleParams);
 442         assertTrue(valid);
 443 
 444         File appOutput = appBundler.execute(appBundleParams, new File(workDir, "PKGExternalApp1"));
 445         System.err.println("App at - " + appOutput);
 446         assertNotNull(appOutput);
 447         assertTrue(appOutput.exists());
 448 
 449         // now create the PKG referencing this external app
 450         Bundler pkgBundler = new MacPkgBundler();
 451 
 452         Map<String, Object> pkgBundleParams = new HashMap<>();
 453 
 454         pkgBundleParams.put(BUILD_ROOT.getID(), tmpBase);
 455 
 456         pkgBundleParams.put(MAC_APP_IMAGE.getID(), appOutput);
 457         pkgBundleParams.put(APP_NAME.getID(), "External APP PKG Test");
 458         pkgBundleParams.put(IDENTIFIER.getID(), "com.example.pkg.external");
 459 
 460         pkgBundleParams.put(VERBOSE.getID(), true);
 461 
 462         if (runtimeJdk != null) {
 463             pkgBundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 464         }
 465         if (keychain != null) {
 466             pkgBundleParams.put(SIGNING_KEYCHAIN.getID(), keychain);
 467         }
 468 
 469         valid = pkgBundler.validate(pkgBundleParams);
 470         assertTrue(valid);
 471 
 472         File pkgOutput = pkgBundler.execute(pkgBundleParams, new File(workDir, "PKGExternalApp2"));
 473         System.err.println(".pkg at - " + pkgOutput);
 474         assertNotNull(pkgOutput);
 475         assertTrue(pkgOutput.exists());
 476         assertTrue(pkgOutput.length() > MIN_SIZE);
 477 
 478         if (signingKeysPresent || keychain != null) {
 479             validateSignatures(pkgOutput);
 480         }
 481 
 482     }
 483 
 484     @Test(expected = ConfigException.class)
 485     public void externanNoAppName() throws ConfigException, UnsupportedPlatformException {
 486         Bundler pkgBundler = new MacPkgBundler();
 487 
 488         Map<String, Object> pkgBundleParams = new HashMap<>();
 489 
 490         pkgBundleParams.put(BUILD_ROOT.getID(), tmpBase);
 491 
 492         pkgBundleParams.put(MAC_APP_IMAGE.getID(), ".");
 493         pkgBundleParams.put(IDENTIFIER.getID(), "net.example.bogus");
 494         pkgBundleParams.put(VERBOSE.getID(), true);
 495 
 496         pkgBundler.validate(pkgBundleParams);
 497     }
 498 
 499     @Test(expected = ConfigException.class)
 500     public void externanNoID() throws ConfigException, UnsupportedPlatformException {
 501         Bundler pkgBundler = new MacPkgBundler();
 502 
 503         Map<String, Object> pkgBundleParams = new HashMap<>();
 504 
 505         pkgBundleParams.put(BUILD_ROOT.getID(), tmpBase);
 506 
 507         pkgBundleParams.put(MAC_APP_IMAGE.getID(), ".");
 508         pkgBundleParams.put(APP_NAME.getID(), "Bogus App");
 509         pkgBundleParams.put(VERBOSE.getID(), true);
 510 
 511         pkgBundler.validate(pkgBundleParams);
 512     }
 513 
 514     @Test(expected = ConfigException.class)
 515     public void invalidLicenseFile() throws ConfigException, UnsupportedPlatformException {
 516         Bundler bundler = new MacPkgBundler();
 517 
 518         Map<String, Object> bundleParams = new HashMap<>();
 519 
 520         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 521 
 522         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 523         bundleParams.put(LICENSE_FILE.getID(), "BOGUS_LICENSE");
 524 
 525         bundler.validate(bundleParams);
 526     }
 527 
 528     /**
 529      * Test a misconfiguration where signature is requested but no key is specified.
 530      */
 531     @Test
 532     public void signButNoCert() throws IOException, ConfigException, UnsupportedPlatformException {
 533         // only run with full tests
 534         Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("FULL_TEST")));
 535 
 536         try {
 537             // first create the external app
 538             Bundler appBundler = new MacAppBundler();
 539 
 540             Map<String, Object> appBundleParams = new HashMap<>();
 541 
 542             appBundleParams.put(BUILD_ROOT.getID(), tmpBase);
 543 
 544             appBundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 545             appBundleParams.put(APP_NAME.getID(), "External APP PKG Negative Signature Test");
 546             appBundleParams.put(IDENTIFIER.getID(), "com.example.pkg.external");
 547             appBundleParams.put(VERBOSE.getID(), true);
 548 
 549             if (runtimeJdk != null) {
 550                 appBundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 551             }
 552 
 553             boolean valid = appBundler.validate(appBundleParams);
 554             assertTrue(valid);
 555 
 556             File appOutput = appBundler.execute(appBundleParams, new File(workDir, "PKGExternalAppSignTest"));
 557             System.err.println("App at - " + appOutput);
 558             assertNotNull(appOutput);
 559             assertTrue(appOutput.exists());
 560 
 561             // now create the PKG referencing this external app
 562             Bundler pkgBundler = new MacPkgBundler();
 563 
 564             Map<String, Object> pkgBundleParams = new HashMap<>();
 565 
 566             pkgBundleParams.put(BUILD_ROOT.getID(), tmpBase);
 567 
 568             pkgBundleParams.put(MAC_APP_IMAGE.getID(), appOutput);
 569             pkgBundleParams.put(APP_NAME.getID(), "Negative Signature Test");
 570             pkgBundleParams.put(IDENTIFIER.getID(), "com.example.pkg.external");
 571 
 572             pkgBundleParams.put(SIGN_BUNDLE.getID(), true);
 573             pkgBundleParams.put(DEVELOPER_ID_INSTALLER_SIGNING_KEY.getID(), null);
 574 
 575             pkgBundler.validate(pkgBundleParams);
 576 
 577             // if we get here we fail
 578             assertTrue("ConfigException should have been thrown", false);
 579         } catch (ConfigException ignore) {
 580             // expected
 581         }
 582     }
 583 
 584     @Test
 585     public void configureEverything() throws Exception {
 586         AbstractBundler bundler = new MacPkgBundler();
 587         Collection<BundlerParamInfo<?>> parameters = bundler.getBundleParameters();
 588 
 589         Map<String, Object> bundleParams = new HashMap<>();
 590 
 591         bundleParams.put(APP_NAME.getID(), "Everything App Name");
 592         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 593         bundleParams.put(ARGUMENTS.getID(), Arrays.asList("He Said", "She Said"));
 594         bundleParams.put(BUNDLE_ID_SIGNING_PREFIX.getID(), "everything.signing.prefix.");
 595         bundleParams.put(CLASSPATH.getID(), "mainApp.jar");
 596         bundleParams.put(ICON_ICNS.getID(), hdpiIcon);
 597         bundleParams.put(INSTALLER_SUFFIX.getID(), "-PKG-TEST");
 598         bundleParams.put(JVM_OPTIONS.getID(), "-Xms128M");
 599         bundleParams.put(JVM_PROPERTIES.getID(), "everything.jvm.property=everything.jvm.property.value");
 600         bundleParams.put(MAC_CATEGORY.getID(), "public.app-category.developer-tools");
 601         bundleParams.put(MAC_CF_BUNDLE_IDENTIFIER.getID(), "com.example.everything.cf-bundle-identifier");
 602         bundleParams.put(MAC_CF_BUNDLE_NAME.getID(), "Everything CF Bundle Name");
 603         bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk == null ? System.getProperty("java.home") : runtimeJdk);
 604         bundleParams.put(MAIN_CLASS.getID(), "hello.HelloRectangle");
 605         bundleParams.put(MAIN_JAR.getID(), "mainApp.jar");
 606         bundleParams.put(PREFERENCES_ID.getID(), "everything/preferences/id");
 607         bundleParams.put(PRELOADER_CLASS.getID(), "hello.HelloPreloader");
 608         bundleParams.put(SIGNING_KEYCHAIN.getID(), "");
 609         bundleParams.put(USER_JVM_OPTIONS.getID(), "-Xmx=256M\n");
 610         bundleParams.put(VERSION.getID(), "1.2.3.4");
 611 
 612         //bundleParams.put(IDENTIFIER.getID(), "com.example.everything.identifier");
 613         bundleParams.put(DEVELOPER_ID_INSTALLER_SIGNING_KEY.getID(), "Developer ID Installer");
 614         bundleParams.put(LICENSE_FILE.getID(), "LICENSE");
 615         //bundleParams.put(SERVICE_HINT.getID(), false);
 616 
 617         // assert they are set
 618         for (BundlerParamInfo bi :parameters) {
 619             assertNotNull("Bundle args Contains " + bi.getID(), bundleParams.containsKey(bi.getID()));
 620         }
 621 
 622         // and only those are set
 623         bundleParamLoop:
 624         for (String s :bundleParams.keySet()) {
 625             for (BundlerParamInfo<?> bpi : parameters) {
 626                 if (s.equals(bpi.getID())) {
 627                     continue bundleParamLoop;
 628                 }
 629             }
 630             fail("Enumerated parameters does not contain " + s);
 631         }
 632 
 633         // assert they resolve
 634         for (BundlerParamInfo bi :parameters) {
 635             bi.fetchFrom(bundleParams);
 636         }
 637 
 638         // add verbose now that we are done scoping out parameters
 639         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 640         bundleParams.put(VERBOSE.getID(), true);
 641 
 642         // assert it validates
 643         boolean valid = bundler.validate(bundleParams);
 644         assertTrue(valid);
 645 
 646         // only run the bundle with full tests
 647         Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("FULL_TEST")));
 648 
 649         // but first remove signing keys, test servers don't have these...
 650         bundleParams.remove(DEVELOPER_ID_INSTALLER_SIGNING_KEY.getID());
 651 
 652         File result = bundler.execute(bundleParams, new File(workDir, "everything"));
 653         System.err.println("Bundle at - " + result);
 654         assertNotNull(result);
 655         assertTrue(result.exists());
 656         assertTrue(result.length() > MIN_SIZE);
 657     }
 658 
 659     public void validateSignatures(File appLocation) throws IOException {
 660         // Check the signatures with pkgUtil
 661         ProcessBuilder pb = new ProcessBuilder(
 662                 "pkgutil", "--check-signature",
 663                 appLocation.getCanonicalPath());
 664         ByteArrayOutputStream baos = new ByteArrayOutputStream();
 665         PrintStream ps = new PrintStream(baos);
 666         try {
 667             IOUtils.exec(pb, true, false, ps);
 668         } catch (IOException ioe) {
 669             if (signingKeysPresent) {
 670                 // these were real keys, failures are real
 671                 throw ioe;
 672             }
 673             // failure was for bogus key
 674             if (ioe.getMessage().contains("Exec failed with code 1 ")) {
 675                 // this is likely because the key is not signed by apple, lets look
 676                 // ok, look to see if our key is in the output
 677                 if (!baos.toString().contains("1. Developer ID Installer: Insecure Test Cert")) {
 678                     // didn't list our key as #1, must be some other error
 679                     throw ioe;
 680                 }
 681                 // ok, this is expected.  Ignore it.
 682             } else {
 683                 // some other failure, throw the error.
 684                 throw ioe;
 685             }
 686         }
 687     }
 688 
 689 }