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     @Test
 274     public void quarantinedAppTest() throws IOException, ConfigException, UnsupportedPlatformException {
 275 
 276         AbstractBundler bundler = new MacPkgBundler();
 277 
 278         assertNotNull(bundler.getName());
 279         assertNotNull(bundler.getID());
 280         assertNotNull(bundler.getDescription());
 281         //assertNotNull(bundler.getBundleParameters());
 282 
 283         Map<String, Object> bundleParams = new HashMap<>();
 284 
 285         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 286 
 287         bundleParams.put(APP_NAME.getID(), "Quarantine App");
 288         bundleParams.put(MAIN_CLASS.getID(), "hello.HelloRectangle");
 289         bundleParams.put(PREFERENCES_ID.getID(), "the/really/long/preferences/id");
 290         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 291         bundleParams.put(VERBOSE.getID(), true);
 292 
 293         if (runtimeJdk != null) {
 294             bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 295         }
 296 
 297         if (!signingKeysPresent) {
 298             String keychain = createFakeCerts(bundleParams);
 299             Assume.assumeNotNull(keychain);
 300             bundleParams.put(SIGNING_KEYCHAIN.getID(), keychain);
 301         }
 302 
 303         boolean valid = bundler.validate(bundleParams);
 304         assertTrue(valid);
 305 
 306         File result = bundler.execute(bundleParams, new File(workDir, "quarantine"));
 307         System.err.println("Bundle at - " + result);
 308         assertNotNull(result);
 309         assertTrue(result.exists());
 310         assertTrue(result.length() > MIN_SIZE);
 311         validateSignatures(result);
 312 
 313         // mark it as though it's been downloaded
 314         ProcessBuilder pb = new ProcessBuilder(
 315                 "xattr", "-w", "com.apple.quarantine",
 316                 "0000;" + Long.toHexString(System.currentTimeMillis() / 1000L) + ";Java Unit Tests;|com.oracle.jvm.8u",
 317                 result.toString());
 318         IOUtils.exec(pb, true);
 319     }
 320 
 321     /**
 322      * The bare minimum configuration needed to make it work
 323      * <ul>
 324      *     <li>Where to build it</li>
 325      *     <li>The jar containing the application (with a main-class attribute)</li>
 326      * </ul>
 327      *
 328      * All other values will be driven off of those two values.
 329      */
 330     @Test
 331     public void minimumConfig() throws IOException, ConfigException, UnsupportedPlatformException {
 332         Bundler bundler = new MacPkgBundler();
 333 
 334         Map<String, Object> bundleParams = new HashMap<>();
 335 
 336         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 337 
 338         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 339 
 340         if (runtimeJdk != null) {
 341             bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 342         }
 343 
 344         String keychain = null;
 345         if (!signingKeysPresent) {
 346             keychain = createFakeCerts(bundleParams);
 347             if (keychain != null) {
 348                 bundleParams.put(SIGNING_KEYCHAIN.getID(), keychain);
 349             }
 350         }
 351 
 352         boolean valid = bundler.validate(bundleParams);
 353         assertTrue(valid);
 354 
 355         File output = bundler.execute(bundleParams, new File(workDir, "BareMinimum"));
 356         System.err.println("Bundle at - " + output);
 357         assertNotNull(output);
 358         assertTrue(output.exists());
 359         assertTrue(output.length() > MIN_SIZE);
 360         if (signingKeysPresent || keychain != null) {
 361             validateSignatures(output);
 362         }
 363     }
 364 
 365     /**
 366      * Test with unicode in places we expect it to be
 367      */
 368     @Ignore("JDK-8163859")
 369     @Test
 370     public void unicodeConfig() throws IOException, ConfigException, UnsupportedPlatformException {
 371         Bundler bundler = new MacPkgBundler();
 372 
 373         Map<String, Object> bundleParams = new HashMap<>();
 374 
 375         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 376 
 377         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 378 
 379         bundleParams.put(APP_NAME.getID(), "хелловорлд");
 380         bundleParams.put(TITLE.getID(), "ХеллоВорлд аппликейшн");
 381         bundleParams.put(VENDOR.getID(), "Оракл девелопмент");
 382         bundleParams.put(DESCRIPTION.getID(), "крайне большое описание со странными символами");
 383 
 384         if (runtimeJdk != null) {
 385             bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 386         }
 387 
 388         String keychain = null;
 389         if (!signingKeysPresent) {
 390             keychain = createFakeCerts(bundleParams);
 391             if (keychain != null) {
 392                 bundleParams.put(SIGNING_KEYCHAIN.getID(), keychain);
 393             }
 394         }
 395 
 396         bundler.validate(bundleParams);
 397 
 398         File output = bundler.execute(bundleParams, new File(workDir, "Unicode"));
 399         System.err.println("Bundle at - " + output);
 400         assertNotNull(output);
 401         assertTrue(output.exists());
 402         if (signingKeysPresent || keychain != null) {
 403             validateSignatures(output);
 404         }
 405     }
 406 
 407     /**
 408      * Create a PKG with an external app rather than a self-created one.
 409      */
 410     @Test
 411     public void externalApp() throws IOException, ConfigException, UnsupportedPlatformException {
 412         // only run with full tests
 413         Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("FULL_TEST")));
 414 
 415         // first create the external app
 416         Bundler appBundler = new MacAppBundler();
 417 
 418         Map<String, Object> appBundleParams = new HashMap<>();
 419 
 420         appBundleParams.put(BUILD_ROOT.getID(), tmpBase);
 421 
 422         appBundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 423         appBundleParams.put(APP_NAME.getID(), "External APP PKG Test");
 424         appBundleParams.put(IDENTIFIER.getID(), "com.example.pkg.external");
 425         appBundleParams.put(VERBOSE.getID(), true);
 426 
 427         if (runtimeJdk != null) {
 428             appBundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 429         }
 430 
 431         String keychain = null;
 432         if (!signingKeysPresent) {
 433             keychain = createFakeCerts(appBundleParams);
 434             if (keychain != null) {
 435                 appBundleParams.put(SIGNING_KEYCHAIN.getID(), keychain);
 436             }
 437         }
 438 
 439         boolean valid = appBundler.validate(appBundleParams);
 440         assertTrue(valid);
 441 
 442         File appOutput = appBundler.execute(appBundleParams, new File(workDir, "PKGExternalApp1"));
 443         System.err.println("App at - " + appOutput);
 444         assertNotNull(appOutput);
 445         assertTrue(appOutput.exists());
 446 
 447         // now create the PKG referencing this external app
 448         Bundler pkgBundler = new MacPkgBundler();
 449 
 450         Map<String, Object> pkgBundleParams = new HashMap<>();
 451 
 452         pkgBundleParams.put(BUILD_ROOT.getID(), tmpBase);
 453 
 454         pkgBundleParams.put(MAC_APP_IMAGE.getID(), appOutput);
 455         pkgBundleParams.put(APP_NAME.getID(), "External APP PKG Test");
 456         pkgBundleParams.put(IDENTIFIER.getID(), "com.example.pkg.external");
 457 
 458         pkgBundleParams.put(VERBOSE.getID(), true);
 459 
 460         if (runtimeJdk != null) {
 461             pkgBundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 462         }
 463         if (keychain != null) {
 464             pkgBundleParams.put(SIGNING_KEYCHAIN.getID(), keychain);
 465         }
 466 
 467         valid = pkgBundler.validate(pkgBundleParams);
 468         assertTrue(valid);
 469 
 470         File pkgOutput = pkgBundler.execute(pkgBundleParams, new File(workDir, "PKGExternalApp2"));
 471         System.err.println(".pkg at - " + pkgOutput);
 472         assertNotNull(pkgOutput);
 473         assertTrue(pkgOutput.exists());
 474         assertTrue(pkgOutput.length() > MIN_SIZE);
 475 
 476         if (signingKeysPresent || keychain != null) {
 477             validateSignatures(pkgOutput);
 478         }
 479 
 480     }
 481 
 482     @Test(expected = ConfigException.class)
 483     public void externanNoAppName() throws ConfigException, UnsupportedPlatformException {
 484         Bundler pkgBundler = new MacPkgBundler();
 485 
 486         Map<String, Object> pkgBundleParams = new HashMap<>();
 487 
 488         pkgBundleParams.put(BUILD_ROOT.getID(), tmpBase);
 489 
 490         pkgBundleParams.put(MAC_APP_IMAGE.getID(), ".");
 491         pkgBundleParams.put(IDENTIFIER.getID(), "net.example.bogus");
 492         pkgBundleParams.put(VERBOSE.getID(), true);
 493 
 494         pkgBundler.validate(pkgBundleParams);
 495     }
 496 
 497     @Test(expected = ConfigException.class)
 498     public void externanNoID() throws ConfigException, UnsupportedPlatformException {
 499         Bundler pkgBundler = new MacPkgBundler();
 500 
 501         Map<String, Object> pkgBundleParams = new HashMap<>();
 502 
 503         pkgBundleParams.put(BUILD_ROOT.getID(), tmpBase);
 504 
 505         pkgBundleParams.put(MAC_APP_IMAGE.getID(), ".");
 506         pkgBundleParams.put(APP_NAME.getID(), "Bogus App");
 507         pkgBundleParams.put(VERBOSE.getID(), true);
 508 
 509         pkgBundler.validate(pkgBundleParams);
 510     }
 511 
 512     @Test(expected = ConfigException.class)
 513     public void invalidLicenseFile() throws ConfigException, UnsupportedPlatformException {
 514         Bundler bundler = new MacPkgBundler();
 515 
 516         Map<String, Object> bundleParams = new HashMap<>();
 517 
 518         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 519 
 520         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 521         bundleParams.put(LICENSE_FILE.getID(), "BOGUS_LICENSE");
 522 
 523         bundler.validate(bundleParams);
 524     }
 525 
 526     /**
 527      * Test a misconfiguration where signature is requested but no key is specified.
 528      */
 529     @Test
 530     public void signButNoCert() throws IOException, ConfigException, UnsupportedPlatformException {
 531         // only run with full tests
 532         Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("FULL_TEST")));
 533 
 534         try {
 535             // first create the external app
 536             Bundler appBundler = new MacAppBundler();
 537 
 538             Map<String, Object> appBundleParams = new HashMap<>();
 539 
 540             appBundleParams.put(BUILD_ROOT.getID(), tmpBase);
 541 
 542             appBundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 543             appBundleParams.put(APP_NAME.getID(), "External APP PKG Negative Signature Test");
 544             appBundleParams.put(IDENTIFIER.getID(), "com.example.pkg.external");
 545             appBundleParams.put(VERBOSE.getID(), true);
 546 
 547             if (runtimeJdk != null) {
 548                 appBundleParams.put(MAC_RUNTIME.getID(), runtimeJdk);
 549             }
 550 
 551             boolean valid = appBundler.validate(appBundleParams);
 552             assertTrue(valid);
 553 
 554             File appOutput = appBundler.execute(appBundleParams, new File(workDir, "PKGExternalAppSignTest"));
 555             System.err.println("App at - " + appOutput);
 556             assertNotNull(appOutput);
 557             assertTrue(appOutput.exists());
 558 
 559             // now create the PKG referencing this external app
 560             Bundler pkgBundler = new MacPkgBundler();
 561 
 562             Map<String, Object> pkgBundleParams = new HashMap<>();
 563 
 564             pkgBundleParams.put(BUILD_ROOT.getID(), tmpBase);
 565 
 566             pkgBundleParams.put(MAC_APP_IMAGE.getID(), appOutput);
 567             pkgBundleParams.put(APP_NAME.getID(), "Negative Signature Test");
 568             pkgBundleParams.put(IDENTIFIER.getID(), "com.example.pkg.external");
 569 
 570             pkgBundleParams.put(SIGN_BUNDLE.getID(), true);
 571             pkgBundleParams.put(DEVELOPER_ID_INSTALLER_SIGNING_KEY.getID(), null);
 572 
 573             pkgBundler.validate(pkgBundleParams);
 574 
 575             // if we get here we fail
 576             assertTrue("ConfigException should have been thrown", false);
 577         } catch (ConfigException ignore) {
 578             // expected
 579         }
 580     }
 581 
 582     @Test
 583     public void configureEverything() throws Exception {
 584         AbstractBundler bundler = new MacPkgBundler();
 585         Collection<BundlerParamInfo<?>> parameters = bundler.getBundleParameters();
 586 
 587         Map<String, Object> bundleParams = new HashMap<>();
 588 
 589         bundleParams.put(APP_NAME.getID(), "Everything App Name");
 590         bundleParams.put(APP_RESOURCES.getID(), new RelativeFileSet(appResourcesDir, appResources));
 591         bundleParams.put(ARGUMENTS.getID(), Arrays.asList("He Said", "She Said"));
 592         bundleParams.put(BUNDLE_ID_SIGNING_PREFIX.getID(), "everything.signing.prefix.");
 593         bundleParams.put(CLASSPATH.getID(), "mainApp.jar");
 594         bundleParams.put(ICON_ICNS.getID(), hdpiIcon);
 595         bundleParams.put(INSTALLER_SUFFIX.getID(), "-PKG-TEST");
 596         bundleParams.put(JVM_OPTIONS.getID(), "-Xms128M");
 597         bundleParams.put(JVM_PROPERTIES.getID(), "everything.jvm.property=everything.jvm.property.value");
 598         bundleParams.put(MAC_CATEGORY.getID(), "public.app-category.developer-tools");
 599         bundleParams.put(MAC_CF_BUNDLE_IDENTIFIER.getID(), "com.example.everything.cf-bundle-identifier");
 600         bundleParams.put(MAC_CF_BUNDLE_NAME.getID(), "Everything CF Bundle Name");
 601         bundleParams.put(MAC_RUNTIME.getID(), runtimeJdk == null ? System.getProperty("java.home") : runtimeJdk);
 602         bundleParams.put(MAIN_CLASS.getID(), "hello.HelloRectangle");
 603         bundleParams.put(MAIN_JAR.getID(), "mainApp.jar");
 604         bundleParams.put(PREFERENCES_ID.getID(), "everything/preferences/id");
 605         bundleParams.put(PRELOADER_CLASS.getID(), "hello.HelloPreloader");
 606         bundleParams.put(SIGNING_KEYCHAIN.getID(), "");
 607         bundleParams.put(USER_JVM_OPTIONS.getID(), "-Xmx=256M\n");
 608         bundleParams.put(VERSION.getID(), "1.2.3.4");
 609 
 610         //bundleParams.put(IDENTIFIER.getID(), "com.example.everything.identifier");
 611         bundleParams.put(DEVELOPER_ID_INSTALLER_SIGNING_KEY.getID(), "Developer ID Installer");
 612         bundleParams.put(LICENSE_FILE.getID(), "LICENSE");
 613         //bundleParams.put(SERVICE_HINT.getID(), false);
 614 
 615         // assert they are set
 616         for (BundlerParamInfo bi :parameters) {
 617             assertNotNull("Bundle args Contains " + bi.getID(), bundleParams.containsKey(bi.getID()));
 618         }
 619 
 620         // and only those are set
 621         bundleParamLoop:
 622         for (String s :bundleParams.keySet()) {
 623             for (BundlerParamInfo<?> bpi : parameters) {
 624                 if (s.equals(bpi.getID())) {
 625                     continue bundleParamLoop;
 626                 }
 627             }
 628             fail("Enumerated parameters does not contain " + s);
 629         }
 630 
 631         // assert they resolve
 632         for (BundlerParamInfo bi :parameters) {
 633             bi.fetchFrom(bundleParams);
 634         }
 635 
 636         // add verbose now that we are done scoping out parameters
 637         bundleParams.put(BUILD_ROOT.getID(), tmpBase);
 638         bundleParams.put(VERBOSE.getID(), true);
 639 
 640         // assert it validates
 641         boolean valid = bundler.validate(bundleParams);
 642         assertTrue(valid);
 643 
 644         // only run the bundle with full tests
 645         Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("FULL_TEST")));
 646 
 647         // but first remove signing keys, test servers don't have these...
 648         bundleParams.remove(DEVELOPER_ID_INSTALLER_SIGNING_KEY.getID());
 649 
 650         File result = bundler.execute(bundleParams, new File(workDir, "everything"));
 651         System.err.println("Bundle at - " + result);
 652         assertNotNull(result);
 653         assertTrue(result.exists());
 654         assertTrue(result.length() > MIN_SIZE);
 655     }
 656 
 657     public void validateSignatures(File appLocation) throws IOException {
 658         // Check the signatures with pkgUtil
 659         ProcessBuilder pb = new ProcessBuilder(
 660                 "pkgutil", "--check-signature",
 661                 appLocation.getCanonicalPath());
 662         ByteArrayOutputStream baos = new ByteArrayOutputStream();
 663         PrintStream ps = new PrintStream(baos);
 664         try {
 665             IOUtils.exec(pb, true, false, ps);
 666         } catch (IOException ioe) {
 667             if (signingKeysPresent) {
 668                 // these were real keys, failures are real
 669                 throw ioe;
 670             }
 671             // failure was for bogus key
 672             if (ioe.getMessage().contains("Exec failed with code 1 ")) {
 673                 // this is likely because the key is not signed by apple, lets look
 674                 // ok, look to see if our key is in the output
 675                 if (!baos.toString().contains("1. Developer ID Installer: Insecure Test Cert")) {
 676                     // didn't list our key as #1, must be some other error
 677                     throw ioe;
 678                 }
 679                 // ok, this is expected.  Ignore it.
 680             } else {
 681                 // some other failure, throw the error.
 682                 throw ioe;
 683             }
 684         }
 685     }
 686 
 687 }