jdk/test/jdk/tools/jpackage/macosx/MacSignTest.java
Alexey Semenyuk 624739e3e5 8382684: jpackage: assorted fixes of the test lib
Reviewed-by: almatvee
2026-04-22 01:54:54 +00:00

420 lines
19 KiB
Java

/*
* Copyright (c) 2025, 2026, 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 static jdk.jpackage.test.MacHelper.SignKeyOption.Type.SIGN_KEY_IDENTITY;
import static jdk.jpackage.test.MacHelper.SignKeyOption.Type.SIGN_KEY_USER_FULL_NAME;
import static jdk.jpackage.test.MacHelper.SignKeyOption.Type.SIGN_KEY_USER_SHORT_NAME;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.Annotations.ParameterSupplier;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.FailedCommandErrorValidator;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.JPackageOutputValidator;
import jdk.jpackage.test.JPackageStringBundle;
import jdk.jpackage.test.MacHelper;
import jdk.jpackage.test.MacHelper.NamedCertificateRequestSupplier;
import jdk.jpackage.test.MacHelper.ResolvableCertificateRequest;
import jdk.jpackage.test.MacHelper.SignKeyOption;
import jdk.jpackage.test.MacHelper.SignKeyOptionWithKeychain;
import jdk.jpackage.test.MacSign;
import jdk.jpackage.test.MacSignVerify;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.TKit;
/*
* @test
* @summary jpackage with --mac-sign
* @library /test/jdk/tools/jpackage/helpers
* @build jdk.jpackage.test.*
* @compile -Xlint:all -Werror SigningBase.java
* @compile -Xlint:all -Werror MacSignTest.java
* @requires (jpackage.test.MacSignTests == "run")
* @run main/othervm/timeout=720 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=MacSignTest
* --jpt-before-run=SigningBase.verifySignTestEnvReady
*/
public class MacSignTest {
@Test
public static void testAppContentWarning() throws IOException {
// Create app content directory with the name known to fail signing.
// This will trigger jpackage exit with status code "1".
final var appContent = TKit.createTempDirectory("app-content").resolve("foo.1");
Files.createDirectory(appContent);
Files.createFile(appContent.resolve("file"));
final var validator = new JPackageOutputValidator().stderr();
validator.expectMatchingStrings(JPackageStringBundle.MAIN.cannedFormattedString(
"message.codesign.failed.reason.app.content"));
final var xcodeWarning = TKit.assertTextStream(JPackageStringBundle.MAIN.cannedFormattedString(
"message.codesign.failed.reason.xcode.tools").getValue()).predicate(String::equals);
if (!MacHelper.isXcodeDevToolsInstalled()) {
validator.add(xcodeWarning);
}
var keychain = SigningBase.StandardKeychain.MAIN.keychain();
var signingKeyOption = new SignKeyOptionWithKeychain(
SIGN_KEY_IDENTITY,
SigningBase.StandardCertificateRequest.CODESIGN,
keychain);
validator.add(buildSignCommandErrorValidator(signingKeyOption).create());
MacSign.withKeychain(_ -> {
// --app-content and --type app-image
// Expect `message.codesign.failed.reason.app.content` message in the log.
// This is not a fatal error, just a warning.
// To make jpackage fail, specify bad additional content.
JPackageCommand.helloAppImage()
.mutate(MacSignTest::init)
.mutate(validator::applyTo)
.addArguments("--app-content", appContent)
.mutate(signingKeyOption::addTo)
.mutate(cmd -> {
if (MacHelper.isXcodeDevToolsInstalled()) {
// Check there is no warning about missing xcode command line developer tools.
cmd.validateErr(xcodeWarning.copy().negate());
}
}).execute(1);
}, MacSign.Keychain.UsageBuilder::addToSearchList, keychain);
}
@Test
public static void testCodesignUnspecificFailure() throws IOException {
// This test expects jpackage to respond in a specific way on a codesign failure.
// There are a few ways to make jpackage fail signing. One is using an erroneous
// combination of a signing key and a keychain.
var signingKeyOption = new SignKeyOption(
SIGN_KEY_IDENTITY,
SigningBase.StandardCertificateRequest.CODESIGN_ACME_TECH_LTD.certRequest(
SigningBase.StandardKeychain.MAIN.keychain()));
MacSign.withKeychain(keychain -> {
// Build a matcher for jpackage's failed command output.
var errorValidator = buildSignCommandErrorValidator(signingKeyOption, keychain)
.output(String.format("%s: no identity found", signingKeyOption.certRequest().name()))
.create();
JPackageCommand.helloAppImage()
.mutate(MacSignTest::init)
.mutate(errorValidator::applyTo)
.mutate(signingKeyOption::addTo)
.mutate(MacHelper.useKeychain(keychain))
.execute(1);
}, MacSign.Keychain.UsageBuilder::addToSearchList, SigningBase.StandardKeychain.DUPLICATE.keychain());
}
@Test
@Parameter({"IMAGE", "EXPIRED_SIGNING_KEY_USER_NAME"})
@Parameter({"MAC_DMG", "EXPIRED_SIGNING_KEY_USER_NAME"})
@Parameter({"MAC_PKG", "EXPIRED_SIGNING_KEY_USER_NAME", "EXPIRED_SIGNING_KEY_USER_NAME_PKG"})
@Parameter({"MAC_PKG", "EXPIRED_SIGNING_KEY_USER_NAME_PKG", "EXPIRED_SIGNING_KEY_USER_NAME"})
@Parameter({"IMAGE", "EXPIRED_SIGN_IDENTITY"})
@Parameter({"MAC_DMG", "EXPIRED_SIGN_IDENTITY"})
// Test that jpackage doesn't print duplicated error messages.
@Parameter({"MAC_PKG", "EXPIRED_SIGN_IDENTITY"})
@Parameter({"IMAGE", "EXPIRED_CODESIGN_SIGN_IDENTITY"})
@Parameter({"MAC_DMG", "EXPIRED_CODESIGN_SIGN_IDENTITY"})
@Parameter({"MAC_PKG", "EXPIRED_CODESIGN_SIGN_IDENTITY"})
@Parameter({"MAC_PKG", "GOOD_CODESIGN_SIGN_IDENTITY", "EXPIRED_PKG_SIGN_IDENTITY"})
@Parameter({"MAC_PKG", "EXPIRED_CODESIGN_SIGN_IDENTITY", "GOOD_PKG_SIGN_IDENTITY"})
public static void testExpiredCertificate(PackageType type, SignOption... optionIds) {
var options = Stream.of(optionIds).map(SignOption::option).toList();
var badOptions = options.stream().filter(option -> {
return option.certRequest().expired();
}).toList();
MacSign.withKeychain(keychain -> {
final var cmd = MacHelper.useKeychain(JPackageCommand.helloAppImage(), keychain)
.mutate(MacSignTest::init)
.addArguments(options.stream().map(SignKeyOption::asCmdlineArgs).flatMap(List::stream).toList())
.setPackageType(type);
configureOutputValidation(cmd, expectFailures(badOptions), opt -> {
if (!opt.passThrough().orElse(false)) {
return expectConfigurationError("error.certificate.outside-validity-period", opt.certRequest().name());
} else {
var builder = buildSignCommandErrorValidator(opt, keychain);
switch (opt.optionName().orElseThrow()) {
case KEY_IDENTITY_APP_IMAGE, KEY_USER_NAME -> {
builder.output(String.format("%s: no identity found", opt.certRequest().name()));
}
case KEY_IDENTITY_INSTALLER -> {
var regexp = Pattern.compile(String.format(
"^productbuild: error: Cannot write product to \\S+ \\(Could not find appropriate signing identity for “%s” in keychain at “%s”\\.\\)",
Pattern.quote(opt.certRequest().name()),
Pattern.quote(keychain.name())
));
builder.validators(TKit.assertTextStream(regexp));
}
}
return builder.create();
}
}).execute(1);
}, MacSign.Keychain.UsageBuilder::addToSearchList, SigningBase.StandardKeychain.EXPIRED.keychain());
}
@Test
@Parameter({"IMAGE", "GOOD_SIGNING_KEY_USER_NAME"})
@Parameter({"MAC_DMG", "GOOD_SIGNING_KEY_USER_NAME"})
@Parameter({"MAC_PKG", "GOOD_SIGNING_KEY_USER_NAME", "GOOD_SIGNING_KEY_USER_NAME_PKG"})
@Parameter({"MAC_PKG", "GOOD_SIGNING_KEY_USER_NAME_PKG", "GOOD_SIGNING_KEY_USER_NAME"})
@Parameter({"IMAGE", "GOOD_CODESIGN_SIGN_IDENTITY"})
@Parameter({"MAC_PKG", "GOOD_CODESIGN_SIGN_IDENTITY", "GOOD_PKG_SIGN_IDENTITY"})
@Parameter({"MAC_PKG", "GOOD_PKG_SIGN_IDENTITY"})
public static void testMultipleCertificates(PackageType type, SignOption... optionIds) {
var options = Stream.of(optionIds).map(SignOption::option).toList();
MacSign.withKeychain(keychain -> {
final var cmd = MacHelper.useKeychain(JPackageCommand.helloAppImage(), keychain)
.mutate(MacSignTest::init)
.addArguments(options.stream().map(SignKeyOption::asCmdlineArgs).flatMap(List::stream).toList())
.setPackageType(type);
if (List.of(optionIds).equals(List.of(SignOption.GOOD_PKG_SIGN_IDENTITY))) {
/**
* Normally, if multiple signing identities share the same name, signing should
* fail. However, in pass-through signing, when jpackage passes signing
* identities without validation to signing commands, signing a .pkg installer
* with a name matching two signing identities succeeds.
*/
new JPackageOutputValidator().stdout()
.expectMatchingStrings(JPackageStringBundle.MAIN.cannedFormattedString("warning.unsigned.app.image", "pkg"))
.validateEndOfStream()
.applyTo(cmd);
cmd.execute();
return;
}
configureOutputValidation(cmd, expectFailures(options), opt -> {
if (!opt.passThrough().orElse(false)) {
return expectConfigurationError("error.multiple.certs.found", opt.certRequest().name(), keychain.name());
} else {
var builder = buildSignCommandErrorValidator(opt, keychain);
switch (opt.optionName().orElseThrow()) {
case KEY_IDENTITY_APP_IMAGE, KEY_USER_NAME -> {
builder.output(String.format("%s: ambiguous", opt.certRequest().name()));
}
case KEY_IDENTITY_INSTALLER -> {
var regexp = Pattern.compile(String.format(
"^productbuild: error: Cannot write product to \\S+ \\(Could not find appropriate signing identity for “%s” in keychain at “%s”\\.\\)",
Pattern.quote(opt.certRequest().name()),
Pattern.quote(keychain.name())
));
builder.validators(TKit.assertTextStream(regexp));
}
}
return builder.create();
}
}).execute(1);
}, MacSign.Keychain.UsageBuilder::addToSearchList, SigningBase.StandardKeychain.DUPLICATE.keychain());
}
@Test
@ParameterSupplier
@ParameterSupplier("testSelectSigningIdentity_JDK_8371094")
public static void testSelectSigningIdentity(SignKeyOptionWithKeychain signKeyOption) {
MacSign.withKeychain(keychain -> {
final var cmd = JPackageCommand.helloAppImage().setFakeRuntime().mutate(signKeyOption::addTo);
cmd.executeAndAssertImageCreated();
MacSignVerify.verifyAppImageSigned(cmd, signKeyOption.certRequest());
}, MacSign.Keychain.UsageBuilder::addToSearchList, SigningBase.StandardKeychain.MAIN.keychain());
}
public static Collection<Object[]> testSelectSigningIdentity() {
var keychain = SigningBase.StandardKeychain.MAIN.keychain();
return Stream.of(
SigningBase.StandardCertificateRequest.CODESIGN,
SigningBase.StandardCertificateRequest.CODESIGN_UNICODE
).map(certRequest -> {
return Stream.of(
SIGN_KEY_USER_FULL_NAME,
SIGN_KEY_USER_SHORT_NAME
).map(type -> {
return new SignKeyOptionWithKeychain(type, certRequest, keychain);
});
}).flatMap(x -> x).map(v -> {
return new Object[] {v};
}).toList();
}
public static Collection<Object[]> testSelectSigningIdentity_JDK_8371094() {
return List.<Object[]>of(new Object[] {
new SignKeyOptionWithKeychain(
"ACME Technologies Limited",
SigningBase.StandardCertificateRequest.CODESIGN_ACME_TECH_LTD,
SigningBase.StandardKeychain.MAIN.keychain())
});
}
enum SignOption {
EXPIRED_SIGNING_KEY_USER_NAME(SIGN_KEY_USER_SHORT_NAME, SigningBase.StandardCertificateRequest.CODESIGN_EXPIRED),
EXPIRED_SIGNING_KEY_USER_NAME_PKG(SIGN_KEY_USER_SHORT_NAME, SigningBase.StandardCertificateRequest.PKG_EXPIRED),
EXPIRED_SIGN_IDENTITY(SIGN_KEY_USER_FULL_NAME, SigningBase.StandardCertificateRequest.CODESIGN_EXPIRED),
EXPIRED_CODESIGN_SIGN_IDENTITY(SIGN_KEY_IDENTITY, SigningBase.StandardCertificateRequest.CODESIGN_EXPIRED),
EXPIRED_PKG_SIGN_IDENTITY(SIGN_KEY_IDENTITY, SigningBase.StandardCertificateRequest.PKG_EXPIRED),
GOOD_SIGNING_KEY_USER_NAME(SIGN_KEY_USER_SHORT_NAME, SigningBase.StandardCertificateRequest.CODESIGN),
GOOD_SIGNING_KEY_USER_NAME_PKG(SIGN_KEY_USER_SHORT_NAME, SigningBase.StandardCertificateRequest.PKG),
GOOD_CODESIGN_SIGN_IDENTITY(SIGN_KEY_IDENTITY, SigningBase.StandardCertificateRequest.CODESIGN),
GOOD_PKG_SIGN_IDENTITY(SIGN_KEY_IDENTITY, SigningBase.StandardCertificateRequest.PKG);
SignOption(SignKeyOption.Type optionType, NamedCertificateRequestSupplier certRequestSupplier) {
this.option = new SignKeyOption(optionType, new ResolvableCertificateRequest(certRequestSupplier.certRequest(), _ -> {
throw new UnsupportedOperationException();
}, certRequestSupplier.name()));
}
SignKeyOption option() {
return option;
}
private final SignKeyOption option;
}
private static JPackageCommand configureOutputValidation(JPackageCommand cmd, Stream<SignKeyOption> options,
Function<SignKeyOption, JPackageOutputValidator> conv) {
// Validate jpackage's output matches expected output.
var outputValidator = new JPackageOutputValidator().stderr();
options.sorted(Comparator.comparing(option -> {
return option.certRequest().type();
})).map(conv).forEach(outputValidator::add);
outputValidator.validateEndOfStream().applyTo(cmd);
return cmd;
}
private static Stream<SignKeyOption> expectFailures(Collection<SignKeyOption> options) {
if (!options.stream().map(option -> {
return option.passThrough().orElse(false);
}).distinct().reduce((x, y) -> {
throw new IllegalArgumentException();
}).get()) {
// No pass-through signing options.
// This means jpackage will validate them at the configuration phase and report all detected errors.
return options.stream();
}
// Pass-through signing options.
// jpackage will not validate them and will report only one error at the packaging phase.
Function<MacSign.CertificateType, Predicate<SignKeyOption>> filterType = type -> {
return option -> {
return option.certRequest().type() == type;
};
};
var appImageSignOption = options.stream()
.filter(filterType.apply(MacSign.CertificateType.CODE_SIGN)).findFirst();
var pkgSignOption = options.stream()
.filter(filterType.apply(MacSign.CertificateType.INSTALLER)).findFirst();
if (appImageSignOption.isPresent() && pkgSignOption.isPresent()) {
return options.stream().filter(option -> {
return appImageSignOption.get() == option;
});
} else {
return options.stream();
}
}
private static JPackageOutputValidator expectConfigurationError(String key, Object ... args) {
return new JPackageOutputValidator().expectMatchingStrings(JPackageCommand.makeError(key, args)).stderr();
}
private static FailedCommandErrorValidator buildSignCommandErrorValidator(SignKeyOptionWithKeychain option) {
return buildSignCommandErrorValidator(option.signKeyOption(), option.keychain());
}
private static FailedCommandErrorValidator buildSignCommandErrorValidator(SignKeyOption option, MacSign.ResolvedKeychain keychain) {
Objects.requireNonNull(option);
Objects.requireNonNull(keychain);
String cmdlinePatternFormat;
String signIdentity = switch (option.optionName().orElseThrow()) {
case KEY_IDENTITY_APP_IMAGE, KEY_USER_NAME -> {
cmdlinePatternFormat = "^/usr/bin/codesign -s %s -vvvv --timestamp --options runtime --prefix \\S+ --keychain %s";
if (option.passThrough().orElse(false)) {
yield String.format("'%s'", option.asCmdlineArgs().getLast());
} else {
yield MacSign.CertificateHash.of(option.certRequest().cert()).toString();
}
}
case KEY_IDENTITY_INSTALLER -> {
cmdlinePatternFormat = "^/usr/bin/productbuild --resources \\S+ --sign %s --keychain %s";
yield String.format("'%s'", option.asCmdlineArgs().getLast());
}
};
return new FailedCommandErrorValidator(Pattern.compile(String.format(
cmdlinePatternFormat,
Pattern.quote(signIdentity),
Pattern.quote(keychain.name())
))).exitCode(1);
}
private static void init(JPackageCommand cmd) {
cmd.setFakeRuntime().ignoreDefaultVerbose(true);
}
}