# HG changeset patch # User herrick # Date 1570215362 14400 # Node ID d82489644b15259d265c5f281dc19081566c3c36 # Parent 4e71249f291c81354bab688bf66851425e401524 8215895: Verify and create tests for Mac installer specific signing options Submitted-by: almatvee Reviewed-by: herrick, asemenyuk diff -r 4e71249f291c -r d82489644b15 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacCertificate.java --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacCertificate.java Fri Oct 04 14:53:39 2019 -0400 +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacCertificate.java Fri Oct 04 14:56:02 2019 -0400 @@ -25,14 +25,10 @@ package jdk.jpackage.internal; -import java.io.BufferedOutputStream; -import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStreamReader; import java.io.PrintStream; import java.nio.file.StandardCopyOption; import java.nio.file.Files; @@ -45,21 +41,21 @@ import java.util.List; import java.util.Locale; -final class MacCertificate { +public final class MacCertificate { private final String certificate; private final boolean verbose; - MacCertificate(String certificate) { + public MacCertificate(String certificate) { this.certificate = certificate; this.verbose = false; } - MacCertificate(String certificate, boolean verbose) { + public MacCertificate(String certificate, boolean verbose) { this.certificate = certificate; this.verbose = verbose; } - boolean isValid() { + public boolean isValid() { return verifyCertificate(this.certificate, verbose); } diff -r 4e71249f291c -r d82489644b15 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Main.java --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Main.java Fri Oct 04 14:53:39 2019 -0400 +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Main.java Fri Oct 04 14:56:02 2019 -0400 @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Predicate; import java.util.stream.Collectors; import static jdk.jpackage.test.TestBuilder.CMDLINE_ARG_PREFIX; @@ -72,26 +73,49 @@ TKit.runTests(tests); - final long failedCount = tests.stream().filter(Functional.identity( - TestInstance::passed).negate()).count(); - final long passedCount = tests.size() - failedCount; - + final long passedCount = tests.stream().filter(TestInstance::passed).count(); TKit.log(String.format("[==========] %d tests ran", tests.size())); TKit.log(String.format("[ PASSED ] %d %s", passedCount, passedCount == 1 ? "test" : "tests")); - if (failedCount != 0) { - TKit.log(String.format("[ FAILED ] %d %s, listed below", - failedCount, - failedCount == 1 ? "test" : "tests")); - tests.stream().filter( - Functional.identity(TestInstance::passed).negate()).forEach( - test -> TKit.log(String.format("[ FAILED ] %s", - test.fullName()))); + + reportDetails(tests, "[ SKIPPED ]", TestInstance::skipped); + reportDetails(tests, "[ FAILED ]", TestInstance::failed); + + var withSkipped = reportSummary(tests, "SKIPPED", TestInstance::skipped); + var withFailures = reportSummary(tests, "FAILED", TestInstance::failed); - final String errorMessage = String.format("%d FAILED %s", - failedCount, failedCount == 1 ? "TEST" : "TESTS"); - TKit.log(errorMessage); - throw new RuntimeException(errorMessage); + if (withFailures != null) { + throw withFailures; + } + + if (withSkipped != null) { + tests.stream().filter(TestInstance::skipped).findFirst().get().rethrowIfSkipped(); } } + + private static long reportDetails(List tests, + String label, Predicate selector) { + final long count = tests.stream().filter(selector).count(); + if (count != 0) { + TKit.log(String.format("%s %d %s, listed below", label, count, count + == 1 ? "test" : "tests")); + tests.stream().filter(selector).forEach(test -> TKit.log( + String.format("%s %s", label, test.fullName()))); + } + + return count; + } + + private static RuntimeException reportSummary(List tests, + String label, Predicate selector) { + final long count = tests.stream().filter(selector).count(); + if (count != 0) { + final String message = String.format("%d %s %s", count, label, count + == 1 ? "TEST" : "TESTS"); + TKit.log(message); + return new RuntimeException(message); + } + + return null; + } } diff -r 4e71249f291c -r d82489644b15 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java Fri Oct 04 14:53:39 2019 -0400 +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java Fri Oct 04 14:56:02 2019 -0400 @@ -70,6 +70,7 @@ TestInstance test = new TestInstance(testBody); ThrowingRunnable.toRunnable(() -> runTests(List.of(test))).run(); + test.rethrowIfSkipped(); if (!test.passed()) { throw new RuntimeException(); } @@ -366,6 +367,16 @@ throw new IllegalStateException("Unknown platform"); } + public static RuntimeException throwSkippedException(String reason) { + trace("Skip the test: " + reason); + RuntimeException ex = ThrowingSupplier.toSupplier( + () -> (RuntimeException) Class.forName("jtreg.SkippedException").getConstructor( + String.class).newInstance(reason)).get(); + + currentTest.notifySkipped(ex); + throw ex; + } + static void waitForFileCreated(Path fileToWaitFor, long timeoutSeconds) throws IOException { diff -r 4e71249f291c -r d82489644b15 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java Fri Oct 04 14:53:39 2019 -0400 +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java Fri Oct 04 14:56:02 2019 -0400 @@ -34,7 +34,6 @@ import jdk.jpackage.test.Functional.ThrowingRunnable; import jdk.jpackage.test.Functional.ThrowingSupplier; - class TestInstance implements ThrowingRunnable { static class TestDesc { @@ -100,8 +99,20 @@ assertCount++; } + void notifySkipped(RuntimeException ex) { + skippedTestException = ex; + } + boolean passed() { - return success; + return status == Status.Passed; + } + + boolean skipped() { + return status == Status.Skipped; + } + + boolean failed() { + return status == Status.Failed; } String functionName() { @@ -116,6 +127,12 @@ return testDesc.testFullName(); } + void rethrowIfSkipped() { + if (skippedTestException != null) { + throw skippedTestException; + } + } + @Override public void run() throws Throwable { final String fullName = testDesc.testFullName(); @@ -131,10 +148,14 @@ afterActions.forEach(a -> TKit.ignoreExceptions(() -> a.accept( testInstance))); } - success = true; + status = Status.Passed; } finally { - TKit.log(String.format("%s %s; checks=%d", - success ? "[ OK ]" : "[ FAILED ]", fullName, + if (skippedTestException != null) { + status = Status.Skipped; + } else if (status == null) { + status = Status.Failed; + } + TKit.log(String.format("%s %s; checks=%d", status, fullName, assertCount)); } } @@ -150,8 +171,26 @@ return null; } + private enum Status { + Passed("[ OK ]"), + Failed("[ FAILED ]"), + Skipped("[ SKIPPED ]"); + + Status(String msg) { + this.msg = msg; + } + + @Override + public String toString() { + return msg; + } + + private final String msg; + } + private int assertCount; - private boolean success; + private Status status; + private RuntimeException skippedTestException; private final TestDesc testDesc; private final ThrowingFunction testConstructor; private final ThrowingConsumer testBody; diff -r 4e71249f291c -r d82489644b15 test/jdk/tools/jpackage/macosx/SigningAppImageTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/tools/jpackage/macosx/SigningAppImageTest.java Fri Oct 04 14:56:02 2019 -0400 @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Path; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.TKit; + +/** + * Tests generation of app image with --mac-sign and related arguments. Test will + * generate app image and verify signature of main launcher and app bundle itself. + * This test requires that machine is configured with test certificate for + * "Developer ID Application: jpackage.openjdk.java.net" in jpackagerTest keychain with + * always allowed access to this keychain for user which runs test. + */ + +/* + * @test + * @summary jpackage with --package-type app-image --mac-sign + * @library ../helpers + * @library /test/lib + * @library base + * @build SigningBase + * @build SigningCheck + * @build jtreg.SkippedException + * @build jdk.jpackage.test.* + * @modules jdk.jpackage/jdk.jpackage.internal + * @requires (os.family == "mac") + * @run main/othervm -Xmx512m SigningAppImageTest + */ +public class SigningAppImageTest { + + public static void main(String[] args) throws Exception { + TKit.run(args, () -> { + SigningCheck.checkCertificates(); + + JPackageCommand cmd = JPackageCommand.helloAppImage(); + cmd.addArguments("--mac-sign", "--mac-signing-key-user-name", + SigningBase.DEV_NAME, "--mac-signing-keychain", + "jpackagerTest.keychain"); + cmd.executeAndAssertHelloAppImageCreated(); + + Path launcherPath = cmd.appImage().resolve(cmd.launcherPathInAppImage()); + SigningBase.verifyCodesign(launcherPath, true); + + Path appImage = cmd.appImage(); + SigningBase.verifyCodesign(appImage, true); + SigningBase.verifySpctl(appImage, "exec"); + }); + } +} diff -r 4e71249f291c -r d82489644b15 test/jdk/tools/jpackage/macosx/SigningPackageTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/tools/jpackage/macosx/SigningPackageTest.java Fri Oct 04 14:56:02 2019 -0400 @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Path; +import java.nio.file.Paths; +import jdk.jpackage.test.*; + +/** + * Tests generation of dmg and pkg with --mac-sign and related arguments. Test will + * generate pkg and verifies its signature. It verifies that dmg is not signed, but app + * image inside dmg is signed. This test requires that machine is configured with test + * certificate for "Developer ID Installer: jpackage.openjdk.java.net" in jpackagerTest + * keychain with always allowed access to this keychain for user which runs test. + */ + +/* + * @test + * @summary jpackage with --package-type pkg,dmg --mac-sign + * @library ../helpers + * @library /test/lib + * @library base + * @build SigningBase + * @build SigningCheck + * @build jtreg.SkippedException + * @build jdk.jpackage.test.* + * @modules jdk.jpackage/jdk.jpackage.internal + * @requires (os.family == "mac") + * @run main/othervm -Xmx512m SigningPackageTest + */ +public class SigningPackageTest { + + private static void verifyPKG(JPackageCommand cmd) { + Path outputBundle = cmd.outputBundle(); + SigningBase.verifyPkgutil(outputBundle); + SigningBase.verifySpctl(outputBundle, "install"); + } + + private static void verifyDMG(JPackageCommand cmd) { + Path outputBundle = cmd.outputBundle(); + SigningBase.verifyCodesign(outputBundle, false); + } + + private static void verifyAppImageInDMG(JPackageCommand cmd) throws Exception { + Path disk = Paths.get("/Volumes", cmd.name()); + try { + new Executor() + .setExecutable("/usr/bin/hdiutil") + .addArgument("attach").addArgument(cmd.outputBundle()) + .execute().assertExitCodeIsZero(); + + Path appImageInDMG = disk.resolve(cmd.name() + ".app"); + Path launcherPath = appImageInDMG.resolve(Path.of("Contents", "MacOS", cmd.name())); + SigningBase.verifyCodesign(launcherPath, true); + SigningBase.verifyCodesign(appImageInDMG, true); + SigningBase.verifySpctl(appImageInDMG, "exec"); + } finally { + new Executor() + .setExecutable("/usr/bin/hdiutil") + .addArgument("detach").addArgument(disk) + .execute().assertExitCodeIsZero(); + } + } + + public static void main(String[] args) throws Exception { + TKit.run(args, () -> { + SigningCheck.checkCertificates(); + + new PackageTest() + .configureHelloApp() + .forTypes(PackageType.MAC) + .addInitializer(cmd -> { + cmd.addArguments("--mac-sign", + "--mac-signing-key-user-name", SigningBase.DEV_NAME, + "--mac-signing-keychain", "jpackagerTest.keychain"); + }) + .forTypes(PackageType.MAC_PKG) + .addBundleVerifier(cmd -> { + verifyPKG(cmd); + }) + .forTypes(PackageType.MAC_DMG) + .addBundleVerifier(cmd -> { + verifyDMG(cmd); + verifyAppImageInDMG(cmd); + }) + .run(); + }); + } +} diff -r 4e71249f291c -r d82489644b15 test/jdk/tools/jpackage/macosx/base/SigningBase.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/tools/jpackage/macosx/base/SigningBase.java Fri Oct 04 14:56:02 2019 -0400 @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Path; +import java.util.List; + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.Executor; + +public class SigningBase { + + public static String DEV_NAME = "jpackage.openjdk.java.net"; + public static String APP_CERT + = "Developer ID Application: " + DEV_NAME; + public static String INSTALLER_CERT + = "Developer ID Installer: " + DEV_NAME; + public static String KEYCHAIN = "jpackagerTest.keychain"; + + private static void checkString(List result, String lookupString) { + result.stream() + .filter(line -> line.trim().equals(lookupString)).findFirst().or( + () -> { + TKit.assertUnexpected(String.format( + "Failed to find [%s] string in the output", + lookupString)); + return null; + }); + } + + private static List codesignResult(Path target, boolean signed) { + int exitCode = signed ? 0 : 1; + List result = new Executor() + .setExecutable("codesign") + .addArguments("--verify", "--deep", "--strict", "--verbose=2", + target.toString()) + .saveOutput() + .execute() + .assertExitCodeIs(exitCode).getOutput(); + + return result; + } + + private static void verifyCodesignResult(List result, Path target, + boolean signed) { + result.stream().peek(TKit::trace); + if (signed) { + String lookupString = target.toString() + ": valid on disk"; + checkString(result, lookupString); + lookupString = target.toString() + ": satisfies its Designated Requirement"; + checkString(result, lookupString); + } else { + String lookupString = target.toString() + + ": code object is not signed at all"; + checkString(result, lookupString); + } + } + + private static List spctlResult(Path target, String type) { + List result = new Executor() + .setExecutable("/usr/sbin/spctl") + .addArguments("-vvv", "--assess", "--type", type, + target.toString()) + .executeAndGetOutput(); + + return result; + } + + private static void verifySpctlResult(List result, Path target, String type) { + result.stream().peek(TKit::trace); + String lookupString = target.toString() + ": accepted"; + checkString(result, lookupString); + lookupString = "source=" + DEV_NAME; + checkString(result, lookupString); + if (type.equals("install")) { + lookupString = "origin=" + INSTALLER_CERT; + } else { + lookupString = "origin=" + APP_CERT; + } + checkString(result, lookupString); + } + + private static List pkgutilResult(Path target) { + List result = new Executor() + .setExecutable("/usr/sbin/pkgutil") + .addArguments("--check-signature", + target.toString()) + .executeAndGetOutput(); + + return result; + } + + private static void verifyPkgutilResult(List result) { + result.stream().peek(line -> TKit.trace(line)); + String lookupString = "Status: signed by a certificate trusted for current user"; + checkString(result, lookupString); + lookupString = "1. " + INSTALLER_CERT; + checkString(result, lookupString); + } + + public static void verifyCodesign(Path target, boolean signed) { + List result = codesignResult(target, signed); + verifyCodesignResult(result, target, signed); + } + + public static void verifySpctl(Path target, String type) { + List result = spctlResult(target, type); + verifySpctlResult(result, target, type); + } + + public static void verifyPkgutil(Path target) { + List result = pkgutilResult(target); + verifyPkgutilResult(result); + } + +} diff -r 4e71249f291c -r d82489644b15 test/jdk/tools/jpackage/macosx/base/SigningCheck.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/tools/jpackage/macosx/base/SigningCheck.java Fri Oct 04 14:56:02 2019 -0400 @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.Executor; + +import jdk.jpackage.internal.MacCertificate; + +public class SigningCheck { + + public static void checkCertificates() { + List result = findCertificate(SigningBase.APP_CERT, SigningBase.KEYCHAIN); + String key = findKey(SigningBase.APP_CERT, result); + validateCertificate(key); + validateCertificateTrust(SigningBase.APP_CERT); + + result = findCertificate(SigningBase.INSTALLER_CERT, SigningBase.KEYCHAIN); + key = findKey(SigningBase.INSTALLER_CERT, result); + validateCertificate(key); + validateCertificateTrust(SigningBase.INSTALLER_CERT); + } + + private static List findCertificate(String name, String keyChain) { + List result = new Executor() + .setExecutable("security") + .addArguments("find-certificate", "-c", name, "-a", keyChain) + .executeAndGetOutput(); + + return result; + } + + private static String findKey(String name, List result) { + Pattern p = Pattern.compile("\"alis\"=\"([^\"]+)\""); + Matcher m = p.matcher(result.stream().collect(Collectors.joining())); + if (!m.find()) { + TKit.trace("Did not found a key for '" + name + "'"); + return null; + } + String matchedKey = m.group(1); + if (m.find()) { + TKit.trace("Found more than one key for '" + name + "'"); + return null; + } + TKit.trace("Using key '" + matchedKey); + return matchedKey; + } + + private static void validateCertificate(String key) { + if (key != null) { + MacCertificate certificate = new MacCertificate( + key, true); + if (!certificate.isValid()) { + TKit.throwSkippedException("Certifcate expired: " + key); + } else { + return; + } + } + + TKit.throwSkippedException("Cannot find required certifciates: " + key); + } + + private static void validateCertificateTrust(String name) { + List result = new Executor() + .setExecutable("security") + .addArguments("dump-trust-settings") + .executeAndGetOutput(); + result.stream().peek(TKit::trace); + result.stream() + .filter(line -> line.trim().endsWith(name)).findFirst().orElseThrow( + () -> { + throw TKit.throwSkippedException("Certifcate not trusted by current user: " + + name); + }); + } + +} diff -r 4e71249f291c -r d82489644b15 test/jdk/tools/jpackage/share/AppImagePackageTest.java --- a/test/jdk/tools/jpackage/share/AppImagePackageTest.java Fri Oct 04 14:53:39 2019 -0400 +++ b/test/jdk/tools/jpackage/share/AppImagePackageTest.java Fri Oct 04 14:56:02 2019 -0400 @@ -39,7 +39,7 @@ * @library ../helpers * @build jdk.jpackage.test.* * @modules jdk.jpackage/jdk.jpackage.internal - * @run main/othervm/timeout=360 -Xmx512m AppImagePackageTest + * @run main/othervm/timeout=540 -Xmx512m AppImagePackageTest */ public class AppImagePackageTest { diff -r 4e71249f291c -r d82489644b15 test/jdk/tools/jpackage/share/InstallDirTest.java --- a/test/jdk/tools/jpackage/share/InstallDirTest.java Fri Oct 04 14:53:39 2019 -0400 +++ b/test/jdk/tools/jpackage/share/InstallDirTest.java Fri Oct 04 14:56:02 2019 -0400 @@ -59,7 +59,7 @@ * @build jdk.jpackage.test.* * @compile InstallDirTest.java * @modules jdk.jpackage/jdk.jpackage.internal - * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main + * @run main/othervm/timeout=540 -Xmx512m jdk.jpackage.test.Main * --jpt-run=InstallDirTest.testCommon */