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