8371438: jpackage should handle the case when "--mac-sign" is specified without signing identity options

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2026-02-26 23:59:09 +00:00
parent 526228ca3f
commit 4a6de12b3a
20 changed files with 1397 additions and 137 deletions

View File

@ -29,6 +29,8 @@ import static jdk.jpackage.internal.FromOptions.createPackageBuilder;
import static jdk.jpackage.internal.MacPackagingPipeline.APPLICATION_LAYOUT;
import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasJliLib;
import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasNoBinDir;
import static jdk.jpackage.internal.OptionUtils.isBundlingOperation;
import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_MAC_PKG;
import static jdk.jpackage.internal.cli.StandardBundlingOperation.SIGN_MAC_APP_IMAGE;
import static jdk.jpackage.internal.cli.StandardOption.APPCLASS;
import static jdk.jpackage.internal.cli.StandardOption.ICON;
@ -120,23 +122,39 @@ final class MacFromOptions {
final Optional<SigningIdentityBuilder> pkgSigningIdentityBuilder;
if (sign && (MAC_INSTALLER_SIGN_IDENTITY.containsIn(options) || MAC_SIGNING_KEY_NAME.containsIn(options))) {
if (!sign) {
pkgSigningIdentityBuilder = Optional.empty();
} else if (hasAppImageSignIdentity(options) && !hasPkgInstallerSignIdentity(options)) {
// They explicitly request to sign the app image,
// but don't specify signing identity for signing the PKG package.
// They want signed app image inside of unsigned PKG.
pkgSigningIdentityBuilder = Optional.empty();
} else {
final var signingIdentityBuilder = createSigningIdentityBuilder(options);
MAC_INSTALLER_SIGN_IDENTITY.ifPresentIn(options, signingIdentityBuilder::signingIdentity);
MAC_SIGNING_KEY_NAME.findIn(options).ifPresent(userName -> {
final StandardCertificateSelector domain;
if (appStore) {
domain = StandardCertificateSelector.APP_STORE_PKG_INSTALLER;
} else {
domain = StandardCertificateSelector.PKG_INSTALLER;
}
signingIdentityBuilder.certificateSelector(StandardCertificateSelector.create(userName, domain));
});
MAC_INSTALLER_SIGN_IDENTITY.findIn(options).ifPresentOrElse(
signingIdentityBuilder::signingIdentity,
() -> {
MAC_SIGNING_KEY_NAME.findIn(options).or(() -> {
if (MAC_APP_IMAGE_SIGN_IDENTITY.findIn(options).isPresent()) {
return Optional.empty();
} else {
return Optional.of("");
}
}).ifPresent(userName -> {
final StandardCertificateSelector domain;
if (appStore) {
domain = StandardCertificateSelector.APP_STORE_PKG_INSTALLER;
} else {
domain = StandardCertificateSelector.PKG_INSTALLER;
}
signingIdentityBuilder.certificateSelector(StandardCertificateSelector.create(userName, domain));
});
}
);
pkgSigningIdentityBuilder = Optional.of(signingIdentityBuilder);
} else {
pkgSigningIdentityBuilder = Optional.empty();
}
ApplicationWithDetails app = null;
@ -230,33 +248,47 @@ final class MacFromOptions {
MAC_BUNDLE_IDENTIFIER.ifPresentIn(options, appBuilder::bundleIdentifier);
MAC_APP_CATEGORY.ifPresentIn(options, appBuilder::category);
final boolean sign;
final boolean sign = MAC_SIGN.getFrom(options);
final boolean appStore;
if (PREDEFINED_APP_IMAGE.containsIn(options) && OptionUtils.bundlingOperation(options) != SIGN_MAC_APP_IMAGE) {
if (PREDEFINED_APP_IMAGE.containsIn(options)) {
final var appImageFileOptions = superAppBuilder.externalApplication().orElseThrow().extra();
sign = MAC_SIGN.getFrom(appImageFileOptions);
appStore = MAC_APP_STORE.getFrom(appImageFileOptions);
} else {
sign = MAC_SIGN.getFrom(options);
appStore = MAC_APP_STORE.getFrom(options);
}
appBuilder.appStore(appStore);
if (sign && (MAC_APP_IMAGE_SIGN_IDENTITY.containsIn(options) || MAC_SIGNING_KEY_NAME.containsIn(options))) {
final var signingIdentityBuilder = createSigningIdentityBuilder(options);
MAC_APP_IMAGE_SIGN_IDENTITY.ifPresentIn(options, signingIdentityBuilder::signingIdentity);
MAC_SIGNING_KEY_NAME.findIn(options).ifPresent(userName -> {
final StandardCertificateSelector domain;
if (appStore) {
domain = StandardCertificateSelector.APP_STORE_APP_IMAGE;
} else {
domain = StandardCertificateSelector.APP_IMAGE;
}
final var signOnlyPkgInstaller = sign && (
isBundlingOperation(options, CREATE_MAC_PKG)
&& !hasAppImageSignIdentity(options)
&& hasPkgInstallerSignIdentity(options));
signingIdentityBuilder.certificateSelector(StandardCertificateSelector.create(userName, domain));
});
if (sign && !signOnlyPkgInstaller) {
final var signingIdentityBuilder = createSigningIdentityBuilder(options);
MAC_APP_IMAGE_SIGN_IDENTITY.findIn(options).ifPresentOrElse(
signingIdentityBuilder::signingIdentity,
() -> {
MAC_SIGNING_KEY_NAME.findIn(options).or(() -> {
if (MAC_INSTALLER_SIGN_IDENTITY.containsIn(options)) {
return Optional.empty();
} else {
return Optional.of("");
}
}).ifPresent(userName -> {
final StandardCertificateSelector domain;
if (appStore) {
domain = StandardCertificateSelector.APP_STORE_APP_IMAGE;
} else {
domain = StandardCertificateSelector.APP_IMAGE;
}
signingIdentityBuilder.certificateSelector(StandardCertificateSelector.create(userName, domain));
});
}
);
final var signingBuilder = new AppImageSigningConfigBuilder(signingIdentityBuilder);
if (appStore) {
@ -331,4 +363,12 @@ final class MacFromOptions {
return builder.create(fa);
}
private static boolean hasAppImageSignIdentity(Options options) {
return options.contains(MAC_SIGNING_KEY_NAME) || options.contains(MAC_APP_IMAGE_SIGN_IDENTITY);
}
private static boolean hasPkgInstallerSignIdentity(Options options) {
return options.contains(MAC_SIGNING_KEY_NAME) || options.contains(MAC_INSTALLER_SIGN_IDENTITY);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* 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
@ -32,6 +32,7 @@ import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_APP_IMAGE;
import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_RUNTIME_IMAGE;
import java.nio.file.Path;
import java.util.Objects;
import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.cli.StandardBundlingOperation;
@ -51,4 +52,8 @@ final class OptionUtils {
static StandardBundlingOperation bundlingOperation(Options options) {
return StandardBundlingOperation.valueOf(BUNDLING_OPERATION_DESCRIPTOR.getFrom(options)).orElseThrow();
}
static boolean isBundlingOperation(Options options, StandardBundlingOperation op) {
return bundlingOperation(options).equals(Objects.requireNonNull(op));
}
}

View File

@ -109,7 +109,7 @@ public final class StandardOption {
public static final OptionValue<Boolean> VERBOSE = auxilaryOption("verbose").create();
public static final OptionValue<BundleType> TYPE = option("type", BundleType.class).addAliases("t")
static final OptionValue<BundleType> TYPE = option("type", BundleType.class).addAliases("t")
.scope(StandardBundlingOperation.values()).inScope(NOT_BUILDING_APP_IMAGE)
.converterExceptionFactory(ERROR_WITH_VALUE).converterExceptionFormatString("ERR_InvalidInstallerType")
.converter(str -> {

View File

@ -13,7 +13,7 @@ maxOutputSize = 2000000
# Run jpackage tests on windows platform sequentially.
# Having "share" directory in the list affects tests on other platforms.
# The better option would be:
# if (platfrom == windowws) {
# if (platfrom == windows) {
# exclusiveAccess.dirs=share windows
# }
# but conditionals are not supported by jtreg configuration files.
@ -29,4 +29,6 @@ modules = \
jdk.jpackage/jdk.jpackage.internal.util.function \
jdk.jpackage/jdk.jpackage.internal.resources:+open \
java.base/jdk.internal.util \
java.base/sun.security.util \
java.base/sun.security.x509 \
jdk.jlink/jdk.tools.jlink.internal

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* 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
@ -31,14 +31,20 @@ import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;
import jdk.jpackage.internal.util.PListReader;
import jdk.jpackage.internal.util.XmlUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
public class MacHelperTest {
public class MacHelperTest extends JUnitAdapter {
@Test
public void test_flatMapPList() {
@ -105,6 +111,18 @@ public class MacHelperTest {
), props);
}
@ParameterizedTest
@MethodSource
public void test_appImageSigned(SignedTestSpec spec) {
spec.test(MacHelper::appImageSigned);
}
@ParameterizedTest
@MethodSource
public void test_nativePackageSigned(SignedTestSpec spec) {
spec.test(MacHelper::nativePackageSigned);
}
private static String createPListXml(String ...xml) {
final List<String> content = new ArrayList<>();
content.add("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
@ -125,4 +143,126 @@ public class MacHelperTest {
throw new RuntimeException(ex);
}
}
private static Stream<SignedTestSpec> test_appImageSigned() {
List<SignedTestSpec.Builder> data = new ArrayList<>();
for (var signingIdentityOption : List.of(
List.<String>of(),
List.of("--mac-signing-key-user-name", "foo"),
List.of("--mac-app-image-sign-identity", "foo"),
List.of("--mac-installer-sign-identity", "foo"),
List.of("--mac-installer-sign-identity", "foo", "--mac-app-image-sign-identity", "bar")
)) {
for (var type : List.of(PackageType.IMAGE, PackageType.MAC_DMG, PackageType.MAC_PKG)) {
for (var withMacSign : List.of(true, false)) {
if (signingIdentityOption.contains("--mac-installer-sign-identity") && type != PackageType.MAC_PKG) {
continue;
}
var builder = SignedTestSpec.build().type(type).cmdline(signingIdentityOption);
if (withMacSign) {
builder.cmdline("--mac-sign");
if (Stream.of(
"--mac-signing-key-user-name",
"--mac-app-image-sign-identity"
).anyMatch(signingIdentityOption::contains) || signingIdentityOption.isEmpty()) {
builder.signed();
}
}
data.add(builder);
}
}
}
return data.stream().map(SignedTestSpec.Builder::create);
}
private static Stream<SignedTestSpec> test_nativePackageSigned() {
List<SignedTestSpec.Builder> data = new ArrayList<>();
for (var signingIdentityOption : List.of(
List.<String>of(),
List.of("--mac-signing-key-user-name", "foo"),
List.of("--mac-app-image-sign-identity", "foo"),
List.of("--mac-installer-sign-identity", "foo"),
List.of("--mac-installer-sign-identity", "foo", "--mac-app-image-sign-identity", "bar")
)) {
for (var type : List.of(PackageType.MAC_DMG, PackageType.MAC_PKG)) {
for (var withMacSign : List.of(true, false)) {
if (signingIdentityOption.contains("--mac-installer-sign-identity") && type != PackageType.MAC_PKG) {
continue;
}
var builder = SignedTestSpec.build().type(type).cmdline(signingIdentityOption);
if (withMacSign) {
builder.cmdline("--mac-sign");
if (type == PackageType.MAC_PKG && (Stream.of(
"--mac-signing-key-user-name",
"--mac-installer-sign-identity"
).anyMatch(signingIdentityOption::contains) || signingIdentityOption.isEmpty())) {
builder.signed();
}
}
data.add(builder);
}
}
}
return data.stream().map(SignedTestSpec.Builder::create);
}
private record SignedTestSpec(boolean expectedSigned, PackageType type, List<String> cmdline) {
SignedTestSpec {
Objects.requireNonNull(type);
cmdline.forEach(Objects::requireNonNull);
}
void test(Function<JPackageCommand, Boolean> func) {
var actualSigned = func.apply((new JPackageCommand().addArguments(cmdline).setPackageType(type)));
assertEquals(expectedSigned, actualSigned);
}
static Builder build() {
return new Builder();
}
static final class Builder {
SignedTestSpec create() {
return new SignedTestSpec(
expectedSigned,
Optional.ofNullable(type).orElse(PackageType.IMAGE),
cmdline);
}
Builder signed() {
expectedSigned = true;
return this;
}
Builder type(PackageType v) {
type = v;
return this;
}
Builder cmdline(String... args) {
return cmdline(List.of(args));
}
Builder cmdline(List<String> v) {
cmdline.addAll(v);
return this;
}
private boolean expectedSigned;
private PackageType type;
private List<String> cmdline = new ArrayList<>();
}
}
}

View File

@ -1292,7 +1292,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
}),
MAC_BUNDLE_UNSIGNED_SIGNATURE(cmd -> {
if (TKit.isOSX() && !MacHelper.appImageSigned(cmd)) {
if (TKit.isOSX()) {
MacHelper.verifyUnsignedBundleSignature(cmd);
}
}),
@ -1316,7 +1316,14 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}),
PREDEFINED_APP_IMAGE_COPY(cmd -> {
Optional.ofNullable(cmd.getArgumentValue("--app-image")).filter(_ -> {
return !TKit.isOSX() || !MacHelper.signPredefinedAppImage(cmd);
if (!TKit.isOSX() || !cmd.hasArgument("--mac-sign")) {
return true;
} else {
var signAppImage = MacHelper.signPredefinedAppImage(cmd)
|| MacHelper.hasAppImageSignIdentity(cmd)
|| MacHelper.isSignWithoutSignIdentity(cmd);
return !signAppImage;
}
}).filter(_ -> {
// Don't examine the contents of the output app image if this is Linux package installing in the "/usr" subtree.
return Optional.<Boolean>ofNullable(cmd.onLinuxPackageInstallDir(null, _ -> false)).orElse(true);

View File

@ -76,6 +76,7 @@ import jdk.jpackage.internal.util.RetryExecutor;
import jdk.jpackage.internal.util.XmlUtils;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
import jdk.jpackage.internal.util.function.ThrowingSupplier;
import jdk.jpackage.test.MacSign.CertificateHash;
import jdk.jpackage.test.MacSign.CertificateRequest;
import jdk.jpackage.test.MacSign.CertificateType;
import jdk.jpackage.test.MacSign.ResolvedKeychain;
@ -259,10 +260,7 @@ public final class MacHelper {
* predefined app image in place and {@code false} otherwise.
*/
public static boolean signPredefinedAppImage(JPackageCommand cmd) {
Objects.requireNonNull(cmd);
if (!TKit.isOSX()) {
throw new UnsupportedOperationException();
}
cmd.verifyIsOfType(PackageType.MAC_DMG, PackageType.MAC_PKG, PackageType.IMAGE);
return cmd.hasArgument("--mac-sign") && cmd.hasArgument("--app-image") && cmd.isImagePackageType();
}
@ -279,10 +277,7 @@ public final class MacHelper {
* otherwise.
*/
public static boolean appImageSigned(JPackageCommand cmd) {
Objects.requireNonNull(cmd);
if (!TKit.isOSX()) {
throw new UnsupportedOperationException();
}
cmd.verifyIsOfType(PackageType.MAC_DMG, PackageType.MAC_PKG, PackageType.IMAGE);
var runtimeImageBundle = Optional.ofNullable(cmd.getArgumentValue("--runtime-image")).map(Path::of).flatMap(MacBundle::fromPath);
var appImage = Optional.ofNullable(cmd.getArgumentValue("--app-image")).map(Path::of);
@ -291,23 +286,102 @@ public final class MacHelper {
// If the predefined runtime is a signed bundle, bundled image should be signed too.
return true;
} else if (appImage.map(MacHelper::isBundleSigned).orElse(false)) {
// The external app image is signed, so the app image is signed too.
// The predefined app image is signed, so the app image is signed too.
return true;
}
if (!cmd.isImagePackageType() && appImage.isPresent()) {
// Building a ".pkg" or a ".dmg" bundle from the predefined app image.
// The predefined app image is unsigned, so the app image bundled
// in the output native package will be unsigned too
// (even if the ".pkg" file may be signed itself, and we never sign ".dmg" files).
return false;
}
if (!cmd.hasArgument("--mac-sign")) {
return false;
} else {
return isSignWithoutSignIdentity(cmd) || hasAppImageSignIdentity(cmd);
}
}
return (cmd.hasArgument("--mac-signing-key-user-name") || cmd.hasArgument("--mac-app-image-sign-identity"));
/**
* Returns {@code true} if the given jpackage command line is configured such
* that the native package it will produce will be signed.
*
* @param cmd the jpackage command to examine
* @return {@code true} if the given jpackage command line is configured such
* the native package it will produce will be signed and {@code false}
* otherwise.
*/
public static boolean nativePackageSigned(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.MAC);
switch (cmd.packageType()) {
case MAC_DMG -> {
return false;
}
case MAC_PKG -> {
if (!cmd.hasArgument("--mac-sign")) {
return false;
} else {
return isSignWithoutSignIdentity(cmd) || hasPkgInstallerSignIdentity(cmd);
}
}
default -> {
throw new IllegalStateException();
}
}
}
/**
* Returns {@code true} if the given jpackage command line has app image signing
* identity option. The command line must have "--mac-sign" option.
*
* @param cmd the jpackage command to examine
* @return {@code true} if the given jpackage command line has app image signing
* identity option and {@code false} otherwise.
*/
public static boolean hasAppImageSignIdentity(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.MAC_DMG, PackageType.MAC_PKG, PackageType.IMAGE);
if (!cmd.hasArgument("--mac-sign")) {
throw new IllegalArgumentException();
}
return Stream.of(
"--mac-signing-key-user-name",
"--mac-app-image-sign-identity"
).anyMatch(cmd::hasArgument);
}
/**
* Returns {@code true} if the given jpackage command line has PKG installer signing
* identity option. The command line must have "--mac-sign" option.
*
* @param cmd the jpackage command to examine
* @return {@code true} if the given jpackage command line has PKG installer signing
* identity option and {@code false} otherwise.
*/
public static boolean hasPkgInstallerSignIdentity(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.MAC_PKG);
if (!cmd.hasArgument("--mac-sign")) {
throw new IllegalArgumentException();
}
return Stream.of(
"--mac-signing-key-user-name",
"--mac-installer-sign-identity"
).anyMatch(cmd::hasArgument);
}
/**
* Returns {@code true} if the given jpackage command line doesn't have signing
* identity options. The command line must have "--mac-sign" option.
*
* @param cmd the jpackage command to examine
* @return {@code true} if the given jpackage command line doesn't have signing
* identity options and {@code false} otherwise.
*/
public static boolean isSignWithoutSignIdentity(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.MAC_DMG, PackageType.MAC_PKG, PackageType.IMAGE);
if (!cmd.hasArgument("--mac-sign")) {
throw new IllegalArgumentException();
}
return Stream.of(
"--mac-signing-key-user-name",
"--mac-app-image-sign-identity",
"--mac-installer-sign-identity"
).noneMatch(cmd::hasArgument);
}
public static void writeFaPListFragment(JPackageCommand cmd, XMLStreamWriter xml) {
@ -702,6 +776,14 @@ public final class MacHelper {
MacSign.CertificateType.CODE_SIGN, Name.KEY_IDENTITY_APP_IMAGE,
MacSign.CertificateType.INSTALLER, Name.KEY_IDENTITY_INSTALLER)),
/**
* "--mac-installer-sign-identity" or "--mac-app-image-sign-identity" option
* with the SHA1 of a signing certificate
*/
SIGN_KEY_IDENTITY_SHA1(Map.of(
MacSign.CertificateType.CODE_SIGN, Name.KEY_IDENTITY_APP_IMAGE,
MacSign.CertificateType.INSTALLER, Name.KEY_IDENTITY_INSTALLER)),
/**
* "--mac-app-image-sign-identity" regardless of the type of signing identity
* (for signing app image or .pkg installer).
@ -714,6 +796,12 @@ public final class MacHelper {
*/
SIGN_KEY_IDENTITY_INSTALLER(Name.KEY_IDENTITY_INSTALLER),
/**
* No explicit option specifying signing identity. jpackage will pick one from
* the specified keychain.
*/
SIGN_KEY_IMPLICIT,
;
Type(Map<MacSign.CertificateType, Name> optionNameMap) {
@ -736,11 +824,24 @@ public final class MacHelper {
return optionNameMapper.apply(Objects.requireNonNull(certType));
}
public boolean passThrough() {
return Stream.of(MacSign.CertificateType.values())
.map(this::mapOptionName)
.flatMap(Optional::stream)
.map(Name::passThrough)
.distinct()
.reduce((_, _) -> {
throw new IllegalStateException();
}).orElse(false);
}
public static Type[] defaultValues() {
return new Type[] {
SIGN_KEY_USER_SHORT_NAME,
SIGN_KEY_USER_FULL_NAME,
SIGN_KEY_IDENTITY
SIGN_KEY_IDENTITY,
SIGN_KEY_IDENTITY_SHA1,
SIGN_KEY_IMPLICIT
};
}
@ -751,7 +852,7 @@ public final class MacHelper {
public String toString() {
var sb = new StringBuilder();
sb.append('{');
applyTo((optionName, _) -> {
type.mapOptionName(certRequest.type()).ifPresent(optionName -> {
sb.append(optionName);
switch (type) {
case SIGN_KEY_USER_FULL_NAME -> {
@ -762,6 +863,9 @@ public final class MacHelper {
sb.append("=").append(ENQUOTER.applyTo(optionValue));
});
}
case SIGN_KEY_IDENTITY_SHA1 -> {
sb.append("/sha1");
}
default -> {
// NOP
}
@ -787,12 +891,16 @@ public final class MacHelper {
}
public List<String> asCmdlineArgs() {
String[] args = new String[2];
applyTo((optionName, optionValue) -> {
args[0] = optionName;
args[1] = optionValue;
});
return List.of(args);
if (type == Type.SIGN_KEY_IMPLICIT) {
return List.of();
} else {
String[] args = new String[2];
applyTo((optionName, optionValue) -> {
args[0] = optionName;
args[1] = optionValue;
});
return List.of(args);
}
}
public Optional<Boolean> passThrough() {
@ -817,6 +925,9 @@ public final class MacHelper {
case SIGN_KEY_USER_SHORT_NAME -> {
return certRequest.shortName();
}
case SIGN_KEY_IDENTITY_SHA1 -> {
return CertificateHash.of(certRequest.cert()).toString();
}
default -> {
throw new IllegalStateException();
}
@ -960,18 +1071,21 @@ public final class MacHelper {
}
static void verifyUnsignedBundleSignature(JPackageCommand cmd) {
if (!cmd.isImagePackageType()) {
if (!cmd.isImagePackageType() && !nativePackageSigned(cmd)) {
MacSignVerify.assertUnsigned(cmd.outputBundle());
}
final Path bundleRoot;
if (cmd.isImagePackageType()) {
bundleRoot = cmd.outputBundle();
} else {
bundleRoot = cmd.pathToUnpackedPackageFile(cmd.appInstallationDirectory());
}
if (!appImageSigned(cmd)) {
final Path bundleRoot;
if (cmd.isImagePackageType()) {
bundleRoot = cmd.outputBundle();
} else {
bundleRoot = cmd.pathToUnpackedPackageFile(cmd.appInstallationDirectory());
}
MacSignVerify.assertAdhocSigned(bundleRoot);
MacSignVerify.assertAdhocSigned(bundleRoot);
}
}
static PackageHandlers createDmgPackageHandlers() {

View File

@ -63,9 +63,11 @@ import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import jdk.jpackage.internal.util.MemoizingSupplier;
import jdk.jpackage.internal.util.function.ExceptionBox;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
import jdk.jpackage.internal.util.function.ThrowingSupplier;
@ -280,8 +282,15 @@ public final class MacSign {
return sb.toString();
}
public static Builder build() {
return new Builder();
}
public static final class Builder {
private Builder() {
}
public Builder name(String v) {
keychainBuilder.name(v);
return this;
@ -306,8 +315,8 @@ public final class MacSign {
return new KeychainWithCertsSpec(keychain, List.copyOf(certs));
}
private Keychain.Builder keychainBuilder = new Keychain.Builder();
private List<CertificateRequest> certs = new ArrayList<>();
private final Keychain.Builder keychainBuilder = Keychain.build();
private final List<CertificateRequest> certs = new ArrayList<>();
}
}
@ -326,8 +335,15 @@ public final class MacSign {
}
}
public static Builder build() {
return new Builder();
}
public static final class Builder {
private Builder() {
}
public Builder name(String v) {
name = v;
return this;
@ -599,12 +615,7 @@ public final class MacSign {
@Override
public String toString() {
final var sb = new StringBuilder();
sb.append(frame("BEGIN " + label));
sb.append(ENCODER.encodeToString(data));
sb.append("\n");
sb.append(frame("END " + label));
return sb.toString();
return PemDataFormatter.format(label, data);
}
static PemData of(X509Certificate cert) {
@ -619,6 +630,21 @@ public final class MacSign {
throw new UncheckedIOException(ex);
}
}
}
private final class PemDataFormatter {
static String format(String label, byte[] data) {
Objects.requireNonNull(label);
Objects.requireNonNull(data);
final var sb = new StringBuilder();
sb.append(frame("BEGIN " + label));
sb.append(ENCODER.encodeToString(data));
sb.append("\n");
sb.append(frame("END " + label));
return sb.toString();
}
private static String frame(String str) {
return String.format("-----%s-----\n", Objects.requireNonNull(str));
@ -627,6 +653,11 @@ public final class MacSign {
private static final Base64.Encoder ENCODER = Base64.getMimeEncoder(64, "\n".getBytes());
}
public static String formatX509Certificate(X509Certificate cert) {
Objects.requireNonNull(cert);
return PemDataFormatter.format("CERTIFICATE", toSupplier(cert::getEncoded).get());
}
public enum DigestAlgorithm {
SHA1(20, () -> MessageDigest.getInstance("SHA-1")),
SHA256(32, () -> MessageDigest.getInstance("SHA-256"));
@ -773,8 +804,15 @@ public final class MacSign {
return COMPARATOR.compare(this, o);
}
public static Builder build() {
return new Builder();
}
public static final class Builder {
private Builder() {
}
public Builder userName(String v) {
userName = v;
return this;
@ -1068,6 +1106,15 @@ public final class MacSign {
return !missingKeychain && !missingCertificates && !invalidCertificates;
}
/**
* Creates an empty keychain with unique name in the work directory of the current test.
*/
public static Keychain createEmptyKeychain() {
return Keychain.build()
.name(TKit.createUniquePath(TKit.workDir().resolve("empty.keychain")).toAbsolutePath().toString())
.create().create();
}
public static Keychain.UsageBuilder withKeychains(KeychainWithCertsSpec... keychains) {
return withKeychains(Stream.of(keychains).map(KeychainWithCertsSpec::keychain).toArray(Keychain[]::new));
}
@ -1100,9 +1147,14 @@ public final class MacSign {
public static void withKeychain(Consumer<ResolvedKeychain> consumer, Consumer<Keychain.UsageBuilder> mutator, ResolvedKeychain keychain) {
Objects.requireNonNull(consumer);
withKeychains(() -> {
Objects.requireNonNull(mutator);
if (keychain.isMock()) {
consumer.accept(keychain);
}, mutator, keychain.spec().keychain());
} else {
withKeychains(() -> {
consumer.accept(keychain);
}, mutator, keychain.spec().keychain());
}
}
public static void withKeychain(Consumer<ResolvedKeychain> consumer, ResolvedKeychain keychain) {
@ -1111,7 +1163,15 @@ public final class MacSign {
public static final class ResolvedKeychain {
public ResolvedKeychain(KeychainWithCertsSpec spec) {
isMock = false;
this.spec = Objects.requireNonNull(spec);
certMapSupplier = MemoizingSupplier.runOnce(() -> {
return MacSign.mapCertificateRequests(spec);
});
}
public static ResolvedKeychain createMock(String name, Map<CertificateRequest, X509Certificate> certs) {
return new ResolvedKeychain(name, certs);
}
public KeychainWithCertsSpec spec() {
@ -1122,15 +1182,30 @@ public final class MacSign {
return spec.keychain().name();
}
public Map<CertificateRequest, X509Certificate> mapCertificateRequests() {
if (certMap == null) {
synchronized (this) {
if (certMap == null) {
certMap = MacSign.mapCertificateRequests(spec);
}
}
public boolean isMock() {
return isMock;
}
public ResolvedKeychain toMock(Map<CertificateRequest, X509Certificate> signEnv) {
if (isMock) {
throw new UnsupportedOperationException("Already a mock");
}
return certMap;
var comm = Comm.compare(Set.copyOf(spec.certificateRequests()), signEnv.keySet());
if (!comm.unique1().isEmpty()) {
throw new IllegalArgumentException(String.format(
"Signing environment missing %s certificate request mappings in [%s] keychain",
comm.unique1(), name()));
}
var certs = new HashMap<>(signEnv);
certs.keySet().retainAll(comm.common());
return createMock(name(), certs);
}
public Map<CertificateRequest, X509Certificate> mapCertificateRequests() {
return certMapSupplier.get();
}
public Function<CertificateRequest, X509Certificate> asCertificateResolver() {
@ -1145,8 +1220,23 @@ public final class MacSign {
};
}
private ResolvedKeychain(String name, Map<CertificateRequest, X509Certificate> certs) {
var keychainBuilder = KeychainWithCertsSpec.build().name(Objects.requireNonNull(name));
certs.keySet().forEach(keychainBuilder::addCert);
var certsCopy = Map.copyOf(Objects.requireNonNull(certs));
isMock = true;
spec = keychainBuilder.create();
certMapSupplier = MemoizingSupplier.runOnce(() -> {
return certsCopy;
});
}
private final boolean isMock;
private final KeychainWithCertsSpec spec;
private volatile Map<CertificateRequest, X509Certificate> certMap;
private final Supplier<Map<CertificateRequest, X509Certificate>> certMapSupplier;
}
private static Map<CertificateRequest, X509Certificate> mapCertificateRequests(KeychainWithCertsSpec spec) {

View File

@ -60,6 +60,36 @@ public interface CommandAction {
public MockIllegalStateException unexpectedArguments() {
return new MockIllegalStateException(String.format("Unexpected arguments: %s", args));
}
public Context shift(int count) {
if (count < 0) {
throw new IllegalArgumentException();
} else if (count == 0) {
return this;
} else {
return new Context(out, err, args.subList(Integer.min(count, args.size()), args.size()));
}
}
public Context shift() {
return shift(1);
}
public void printlnOut(Object obj) {
out.println(obj);
}
public void printlnOut(String str) {
out.println(str);
}
public void printlnErr(Object obj) {
err.println(obj);
}
public void printlnErr(String str) {
err.println(str);
}
}
/**

View File

@ -22,13 +22,15 @@
*/
package jdk.jpackage.test.mock;
import java.util.Objects;
/**
* Indicates command mock internal error.
*/
public final class MockIllegalStateException extends IllegalStateException {
public MockIllegalStateException(String msg) {
super(msg);
super(Objects.requireNonNull(msg));
}
private static final long serialVersionUID = 1L;

View File

@ -0,0 +1,176 @@
/*
* Copyright (c) 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.
*/
package jdk.jpackage.test.stdmock;
import java.nio.file.Path;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.test.MacSign;
import jdk.jpackage.test.MacSign.CertificateRequest;
import jdk.jpackage.test.MacSign.ResolvedKeychain;
import jdk.jpackage.test.mock.CommandAction;
import jdk.jpackage.test.mock.MockIllegalStateException;
/**
* Mocks /usr/bin/security command.
*/
final class MacSecurityMock implements CommandAction {
MacSecurityMock(MacSignMockUtils.SignEnv signEnv) {
Objects.requireNonNull(signEnv);
var keychains = signEnv.keychains();
var stdUserKeychains = Stream.of(StandardKeychain.values()).map(StandardKeychain::keychainName).filter(name -> {
// Add standard keychain unless it is defined in the signing environment.
return keychains.stream().noneMatch(keychain -> {
return keychain.name().equals(name);
});
}).map(name -> {
// Assume the standard keychain is empty.
return ResolvedKeychain.createMock(name, Map.of());
});
allKnownKeychains = Stream.of(
stdUserKeychains,
keychains.stream()
).flatMap(x -> x).collect(Collectors.toUnmodifiableMap(ResolvedKeychain::name, x -> x));
currentKeychains.addAll(Stream.of(StandardKeychain.values())
.map(StandardKeychain::keychainName)
.map(allKnownKeychains::get)
.map(Objects::requireNonNull).toList());
}
@Override
public Optional<Integer> run(Context context) {
switch (context.args().getFirst()) {
case "list-keychains" -> {
listKeychains(context.shift());
return Optional.of(0);
}
case "find-certificate" -> {
findCertificate(context.shift());
return Optional.of(0);
}
default -> {
throw context.unexpectedArguments();
}
}
}
private void listKeychains(Context context) {
if (context.args().getFirst().equals("-s")) {
currentKeychains.clear();
currentKeychains.addAll(context.shift().args().stream().map(name -> {
return Optional.ofNullable(allKnownKeychains.get(name)).orElseThrow(() -> {
throw new MockIllegalStateException(String.format("Unknown keychain name: %s", name));
});
}).toList());
} else if (context.args().isEmpty()) {
currentKeychains.stream().map(keychain -> {
return String.format(" \"%s\"", keychain.name());
}).forEach(context::printlnOut);
} else {
throw context.unexpectedArguments();
}
}
private void findCertificate(Context context) {
var args = new ArrayList<>(context.args());
for (var mandatoryArg : List.of("-p", "-a")) {
if (!args.remove(mandatoryArg)) {
throw context.unexpectedArguments();
}
}
var certNameFilter = context.findOptionValue("-c").map(certNameSubstr -> {
// Remove option name and its value.
var idx = args.indexOf("-c");
args.remove(idx);
args.remove(idx);
Predicate<Map.Entry<CertificateRequest, X509Certificate>> pred = e -> {
return e.getKey().name().contains(certNameSubstr);
};
return pred;
});
Stream<ResolvedKeychain> keychains;
if (args.isEmpty()) {
keychains = currentKeychains.stream();
} else {
// Remaining arguments must be keychain names.
keychains = args.stream().map(keychainName -> {
return Optional.ofNullable(allKnownKeychains.get(keychainName)).orElseThrow(() -> {
throw new MockIllegalStateException(String.format("Unknown keychain name: %s", keychainName));
});
});
}
var certStream = keychains.flatMap(keychain -> {
return keychain.mapCertificateRequests().entrySet().stream();
});
if (certNameFilter.isPresent()) {
certStream = certStream.filter(certNameFilter.get());
}
certStream.map(Map.Entry::getValue).map(MacSign::formatX509Certificate).forEach(formattedCert -> {
context.out().print(formattedCert);
});
}
// Keep the order of the items as the corresponding keychains appear
// in the output of the "/usr/bin/security list-keychains" command.
private enum StandardKeychain {
USER_KEYCHAIN {
@Override
String keychainName() {
return Path.of(System.getProperty("user.home")).resolve("Library/Keychains/login.keychain-db").toString();
}
},
SYSTEM_KEYCHAIN {
@Override
String keychainName() {
return "/Library/Keychains/System.keychain";
}
},
;
abstract String keychainName();
}
private final List<ResolvedKeychain> currentKeychains = new ArrayList<ResolvedKeychain>();
private final Map<String, ResolvedKeychain> allKnownKeychains;
}

View File

@ -0,0 +1,284 @@
/*
* Copyright (c) 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.
*/
package jdk.jpackage.test.stdmock;
import static jdk.jpackage.internal.util.function.ExceptionBox.toUnchecked;
import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction;
import static jdk.jpackage.internal.util.function.ThrowingRunnable.toRunnable;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import jdk.jpackage.internal.util.function.ExceptionBox;
import jdk.jpackage.test.MacSign.CertificateRequest;
import jdk.jpackage.test.MacSign.KeychainWithCertsSpec;
import jdk.jpackage.test.MacSign.ResolvedKeychain;
import jdk.jpackage.test.mock.CommandActionSpec;
import jdk.jpackage.test.mock.CommandActionSpecs;
import jdk.jpackage.test.mock.CommandMockSpec;
/**
* Utilities to create macOS signing tool mocks.
*/
public final class MacSignMockUtils {
private MacSignMockUtils() {
}
public static Map<CertificateRequest, X509Certificate> resolveCertificateRequests(
Collection<CertificateRequest> certificateRequests) {
Objects.requireNonNull(certificateRequests);
var caKeys = createKeyPair();
Function<CertificateRequest, X509Certificate> resolver = toFunction(certRequest -> {
var builder = new CertificateBuilder()
.setSubjectName("CN=" + certRequest.name())
.setPublicKey(caKeys.getPublic())
.setSerialNumber(BigInteger.ONE)
.addSubjectKeyIdExt(caKeys.getPublic())
.addAuthorityKeyIdExt(caKeys.getPublic());
Instant from;
Instant to;
if (certRequest.expired()) {
from = LocalDate.now().minusDays(10).atStartOfDay(ZoneId.systemDefault()).toInstant();
to = from.plus(Duration.ofDays(1));
} else {
from = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant();
to = from.plus(Duration.ofDays(certRequest.days()));
}
builder.setValidity(Date.from(from), Date.from(to));
return builder.build(null, caKeys.getPrivate());
});
return certificateRequests.stream()
.distinct()
.collect(Collectors.toUnmodifiableMap(x -> x, resolver));
}
public static Map<CertificateRequest, X509Certificate> resolveCertificateRequests(
CertificateRequest... certificateRequests) {
return resolveCertificateRequests(List.of(certificateRequests));
}
public static final class SignEnv {
public SignEnv(List<KeychainWithCertsSpec> spec) {
Objects.requireNonNull(spec);
spec.stream().map(keychain -> {
return keychain.keychain().name();
}).collect(Collectors.toMap(x -> x, x -> x, (a, b) -> {
throw new IllegalArgumentException(String.format("Multiple keychains with the same name: %s", a));
}));
this.spec = List.copyOf(spec);
this.env = resolveCertificateRequests(
spec.stream().map(KeychainWithCertsSpec::certificateRequests).flatMap(Collection::stream).toList());
}
public SignEnv(KeychainWithCertsSpec... spec) {
this(List.of(spec));
}
public List<ResolvedKeychain> keychains() {
return spec.stream().map(ResolvedKeychain::new).map(keychain -> {
return keychain.toMock(env);
}).toList();
}
public Map<CertificateRequest, X509Certificate> env() {
return env;
}
private final Map<CertificateRequest, X509Certificate> env;
private final List<KeychainWithCertsSpec> spec;
}
public static CommandMockSpec securityMock(SignEnv signEnv) {
var action = CommandActionSpec.create("/usr/bin/security", new MacSecurityMock(signEnv));
return new CommandMockSpec(action.description(), CommandActionSpecs.build().action(action).create());
}
private static KeyPair createKeyPair() {
try {
var kpg = KeyPairGenerator.getInstance("RSA");
return kpg.generateKeyPair();
} catch (NoSuchAlgorithmException ex) {
throw ExceptionBox.toUnchecked(ex);
}
}
//
// Reflection proxy for jdk.test.lib.security.CertificateBuilder class.
//
// Can't use it directly because it is impossible to cherry-pick this class from the JDK test lib in JUnit tests due to limitations of jtreg.
//
// Shared jpackage JUnit tests don't require "jdk.jpackage.test.stdmock", but they depend on "jdk.jpackage.test" package.
// Source code for these two packages resides in the same directory tree, so jtreg will pull in classes from both packages for the jpackage JUnit tests.
// Static dependency on jdk.test.lib.security.CertificateBuilder class will force pulling in the entire JDK test lib, because of jtreg limitations.
//
// Use dynamic dependency as a workaround. Tests that require jdk.test.lib.security.CertificateBuilder class, should have
//
// /*
// * ...
// * @library /test/lib
// * @build jdk.test.lib.security.CertificateBuilder
// */
//
// in their declarations. They also should have
//
// --add-exports java.base/sun.security.x509=ALL-UNNAMED
// --add-exports java.base/sun.security.util=ALL-UNNAMED
//
// on javac and java command lines.
//
private static final class CertificateBuilder {
CertificateBuilder() {
instance = toSupplier(ctor::newInstance).get();
}
CertificateBuilder setSubjectName(String v) {
toRunnable(() -> {
setSubjectName.invoke(instance, v);
}).run();
return this;
}
CertificateBuilder setPublicKey(PublicKey v) {
toRunnable(() -> {
setPublicKey.invoke(instance, v);
}).run();
return this;
}
CertificateBuilder setSerialNumber(BigInteger v) {
toRunnable(() -> {
setSerialNumber.invoke(instance, v);
}).run();
return this;
}
CertificateBuilder addSubjectKeyIdExt(PublicKey v) {
toRunnable(() -> {
addSubjectKeyIdExt.invoke(instance, v);
}).run();
return this;
}
CertificateBuilder addAuthorityKeyIdExt(PublicKey v) {
toRunnable(() -> {
addAuthorityKeyIdExt.invoke(instance, v);
}).run();
return this;
}
CertificateBuilder setValidity(Date from, Date to) {
toRunnable(() -> {
setValidity.invoke(instance, from, to);
}).run();
return this;
}
X509Certificate build(X509Certificate issuerCert, PrivateKey issuerKey) throws IOException, CertificateException {
try {
return (X509Certificate)toSupplier(() -> {
return build.invoke(instance, issuerCert, issuerKey);
}).get();
} catch (ExceptionBox box) {
switch (ExceptionBox.unbox(box)) {
case IOException ex -> {
throw ex;
}
case CertificateException ex -> {
throw ex;
}
default -> {
throw box;
}
}
}
}
private final Object instance;
private static final Constructor<?> ctor;
private static final Method setSubjectName;
private static final Method setPublicKey;
private static final Method setSerialNumber;
private static final Method addSubjectKeyIdExt;
private static final Method addAuthorityKeyIdExt;
private static final Method setValidity;
private static final Method build;
static {
try {
var certificateBuilderClass = Class.forName("jdk.test.lib.security.CertificateBuilder");
ctor = certificateBuilderClass.getConstructor();
setSubjectName = certificateBuilderClass.getMethod("setSubjectName", String.class);
setPublicKey = certificateBuilderClass.getMethod("setPublicKey", PublicKey.class);
setSerialNumber = certificateBuilderClass.getMethod("setSerialNumber", BigInteger.class);
addSubjectKeyIdExt = certificateBuilderClass.getMethod("addSubjectKeyIdExt", PublicKey.class);
addAuthorityKeyIdExt = certificateBuilderClass.getMethod("addAuthorityKeyIdExt", PublicKey.class);
setValidity = certificateBuilderClass.getMethod("setValidity", Date.class, Date.class);
build = certificateBuilderClass.getMethod("build", X509Certificate.class, PrivateKey.class);
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException ex) {
throw toUnchecked(ex);
}
}
}
}

View File

@ -39,6 +39,16 @@ ErrorTest.test(WIN_MSI; app-desc=Hello; args-add=[--app-version, 1234]; errors=[
ErrorTest.test(WIN_MSI; app-desc=Hello; args-add=[--app-version, 256.1]; errors=[message.error-header+[error.msi-product-version-major-out-of-range, 256.1], message.advice-header+[error.version-string-wrong-format.advice]])
ErrorTest.test(WIN_MSI; app-desc=Hello; args-add=[--launcher-as-service]; errors=[message.error-header+[error.missing-service-installer], message.advice-header+[error.missing-service-installer.advice]])
ErrorTest.test(args-add=[@foo]; errors=[message.error-header+[ERR_CannotParseOptions, foo]])
ErrorTest.testMacSignWithoutIdentity(IMAGE; app-desc=Hello; args-add=[--mac-sign, --mac-signing-keychain, @@EMPTY_KEYCHAIN@@]; errors=[message.error-header+[error.cert.not.found, CODE_SIGN, EMPTY_KEYCHAIN]])
ErrorTest.testMacSignWithoutIdentity(IMAGE; args-add=[--app-image, @@APP_IMAGE_WITH_SHORT_NAME@@, --mac-sign, --mac-signing-keychain, @@EMPTY_KEYCHAIN@@]; errors=[message.error-header+[error.cert.not.found, CODE_SIGN, EMPTY_KEYCHAIN]])
ErrorTest.testMacSignWithoutIdentity(MAC_DMG; app-desc=Hello; args-add=[--mac-sign, --mac-signing-keychain, @@EMPTY_KEYCHAIN@@]; errors=[message.error-header+[error.cert.not.found, CODE_SIGN, EMPTY_KEYCHAIN]])
ErrorTest.testMacSignWithoutIdentity(MAC_DMG; args-add=[--app-image, @@APP_IMAGE_WITH_SHORT_NAME@@, --mac-sign, --mac-signing-keychain, @@EMPTY_KEYCHAIN@@]; errors=[message.error-header+[error.cert.not.found, CODE_SIGN, EMPTY_KEYCHAIN]])
ErrorTest.testMacSignWithoutIdentity(MAC_PKG; app-desc=Hello; args-add=[--mac-sign, --mac-signing-keychain, @@EMPTY_KEYCHAIN@@]; errors=[message.error-header+[error.cert.not.found, CODE_SIGN, EMPTY_KEYCHAIN], message.error-header+[error.cert.not.found, INSTALLER, EMPTY_KEYCHAIN]])
ErrorTest.testMacSignWithoutIdentity(MAC_PKG; app-desc=Hello; args-add=[--mac-sign, --mac-signing-keychain, @@KEYCHAIN_WITH_APP_IMAGE_CERT@@]; args-del=[--name]; errors=[message.error-header+[error.cert.not.found, INSTALLER, KEYCHAIN_WITH_APP_IMAGE_CERT]])
ErrorTest.testMacSignWithoutIdentity(MAC_PKG; app-desc=Hello; args-add=[--mac-sign, --mac-signing-keychain, @@KEYCHAIN_WITH_PKG_CERT@@]; args-del=[--name]; errors=[message.error-header+[error.cert.not.found, CODE_SIGN, KEYCHAIN_WITH_PKG_CERT]])
ErrorTest.testMacSignWithoutIdentity(MAC_PKG; args-add=[--app-image, @@APP_IMAGE_WITH_SHORT_NAME@@, --mac-sign, --mac-signing-keychain, @@EMPTY_KEYCHAIN@@]; errors=[message.error-header+[error.cert.not.found, CODE_SIGN, EMPTY_KEYCHAIN], message.error-header+[error.cert.not.found, INSTALLER, EMPTY_KEYCHAIN]])
ErrorTest.testMacSignWithoutIdentity(MAC_PKG; args-add=[--mac-sign, --mac-signing-keychain, @@KEYCHAIN_WITH_APP_IMAGE_CERT@@, --app-image, @@APP_IMAGE_WITH_SHORT_NAME@@]; errors=[message.error-header+[error.cert.not.found, INSTALLER, KEYCHAIN_WITH_APP_IMAGE_CERT]])
ErrorTest.testMacSignWithoutIdentity(MAC_PKG; args-add=[--mac-sign, --mac-signing-keychain, @@KEYCHAIN_WITH_PKG_CERT@@, --app-image, @@APP_IMAGE_WITH_SHORT_NAME@@]; errors=[message.error-header+[error.cert.not.found, CODE_SIGN, KEYCHAIN_WITH_PKG_CERT]])
ErrorTest.testMacSigningIdentityValidation(IMAGE, --mac-app-image-sign-identity, true)
ErrorTest.testMacSigningIdentityValidation(IMAGE, --mac-signing-key-user-name, false)
ErrorTest.testMacSigningIdentityValidation(MAC_DMG, --mac-app-image-sign-identity, true)

View File

@ -82,10 +82,17 @@ public class SigningAppImageTest {
SigningBase.StandardCertificateRequest.CODESIGN_UNICODE
)) {
for (var signIdentityType : SignKeyOption.Type.defaultValues()) {
data.add(new SignKeyOptionWithKeychain(
signIdentityType,
certRequest,
SigningBase.StandardKeychain.MAIN.keychain()));
SigningBase.StandardKeychain keychain;
if (signIdentityType == SignKeyOption.Type.SIGN_KEY_IMPLICIT) {
keychain = SigningBase.StandardKeychain.SINGLE;
if (!keychain.contains(certRequest)) {
continue;
}
} else {
keychain = SigningBase.StandardKeychain.MAIN;
}
data.add(new SignKeyOptionWithKeychain(signIdentityType, certRequest, keychain.keychain()));
}
}

View File

@ -93,6 +93,11 @@ public class SigningAppImageTwoStepsTest {
return new TestSpec(Optional.ofNullable(signAppImage), sign);
}
Builder keychain(SigningBase.StandardKeychain v) {
keychain = Objects.requireNonNull(v);
return this;
}
Builder certRequest(SigningBase.StandardCertificateRequest v) {
certRequest = Objects.requireNonNull(v);
return this;
@ -117,9 +122,10 @@ public class SigningAppImageTwoStepsTest {
return new SignKeyOptionWithKeychain(
signIdentityType,
certRequest,
SigningBase.StandardKeychain.MAIN.keychain());
keychain.keychain());
}
private SigningBase.StandardKeychain keychain = SigningBase.StandardKeychain.MAIN;
private SigningBase.StandardCertificateRequest certRequest = SigningBase.StandardCertificateRequest.CODESIGN;
private SignKeyOption.Type signIdentityType = SignKeyOption.Type.SIGN_KEY_IDENTITY;
@ -144,18 +150,26 @@ public class SigningAppImageTwoStepsTest {
}, signOption.keychain());
}, appImageCmd::execute);
var cmd = new JPackageCommand()
.setPackageType(PackageType.IMAGE)
.addArguments("--app-image", appImageCmd.outputBundle())
.mutate(sign::addTo);
MacSign.withKeychain(keychain -> {
var cmd = new JPackageCommand()
.setPackageType(PackageType.IMAGE)
.addArguments("--app-image", appImageCmd.outputBundle())
.mutate(sign::addTo);
cmd.executeAndAssertHelloAppImageCreated();
MacSignVerify.verifyAppImageSigned(cmd, sign.certRequest());
cmd.executeAndAssertHelloAppImageCreated();
MacSignVerify.verifyAppImageSigned(cmd, sign.certRequest());
}, sign.keychain());
}
}
public static Collection<Object[]> test() {
var signIdentityTypes = List.of(
SignKeyOption.Type.SIGN_KEY_USER_SHORT_NAME,
SignKeyOption.Type.SIGN_KEY_IDENTITY_APP_IMAGE,
SignKeyOption.Type.SIGN_KEY_IMPLICIT
);
List<TestSpec> data = new ArrayList<>();
for (var appImageSign : withAndWithout(SignKeyOption.Type.SIGN_KEY_IDENTITY)) {
@ -167,9 +181,12 @@ public class SigningAppImageTwoStepsTest {
.certRequest(SigningBase.StandardCertificateRequest.CODESIGN_ACME_TECH_LTD)
.signAppImage();
});
for (var signIdentityType : SignKeyOption.Type.defaultValues()) {
for (var signIdentityType : signIdentityTypes) {
builder.signIdentityType(signIdentityType)
.certRequest(SigningBase.StandardCertificateRequest.CODESIGN);
if (signIdentityType == SignKeyOption.Type.SIGN_KEY_IMPLICIT) {
builder.keychain(SigningBase.StandardKeychain.SINGLE);
}
data.add(builder.sign().create());
}
}

View File

@ -38,7 +38,7 @@ import jdk.jpackage.test.TKit;
* @test
* @summary Setup the environment for jpackage macos signing tests.
* Creates required keychains and signing identities.
* Does NOT run any jpackag tests.
* Does NOT run any jpackage tests.
* @library /test/jdk/tools/jpackage/helpers
* @build jdk.jpackage.test.*
* @compile -Xlint:all -Werror SigningBase.java
@ -51,7 +51,7 @@ import jdk.jpackage.test.TKit;
* @test
* @summary Tear down the environment for jpackage macos signing tests.
* Deletes required keychains and signing identities.
* Does NOT run any jpackag tests.
* Does NOT run any jpackage tests.
* @library /test/jdk/tools/jpackage/helpers
* @build jdk.jpackage.test.*
* @compile -Xlint:all -Werror SigningBase.java
@ -83,7 +83,7 @@ public class SigningBase {
}
private static CertificateRequest.Builder cert() {
return new CertificateRequest.Builder();
return CertificateRequest.build();
}
private final CertificateRequest spec;
@ -118,6 +118,12 @@ public class SigningBase {
StandardCertificateRequest.PKG,
StandardCertificateRequest.CODESIGN_COPY,
StandardCertificateRequest.PKG_COPY),
/**
* A keychain with a single certificate for each role.
*/
SINGLE("jpackagerTest-single.keychain",
StandardCertificateRequest.CODESIGN,
StandardCertificateRequest.PKG),
;
StandardKeychain(String keychainName, StandardCertificateRequest... certs) {
@ -145,7 +151,7 @@ public class SigningBase {
}
private static KeychainWithCertsSpec.Builder keychain(String name) {
return new KeychainWithCertsSpec.Builder().name(name);
return KeychainWithCertsSpec.build().name(name);
}
private static List<KeychainWithCertsSpec> signingEnv() {
@ -164,13 +170,13 @@ public class SigningBase {
}
public static void verifySignTestEnvReady() {
if (!Inner.SIGN_ENV_READY) {
if (!SignEnvReady.VALUE) {
TKit.throwSkippedException(new IllegalStateException("Misconfigured signing test environment"));
}
}
private final class Inner {
private static final boolean SIGN_ENV_READY = MacSign.isDeployed(StandardKeychain.signingEnv());
private final class SignEnvReady {
static final boolean VALUE = MacSign.isDeployed(StandardKeychain.signingEnv());
}
private static final String NAME_ASCII = "jpackage.openjdk.java.net";

View File

@ -94,7 +94,7 @@ public class SigningPackageTest {
}
public static Collection<Object[]> test() {
return TestSpec.testCases(true).stream().map(v -> {
return TestSpec.testCases().stream().map(v -> {
return new Object[] {v};
}).toList();
}
@ -126,7 +126,10 @@ public class SigningPackageTest {
}
if (appImageSignOption.isEmpty()) {
if (packageSignOption.get().type() != SignKeyOption.Type.SIGN_KEY_IDENTITY) {
if (!List.of(
SignKeyOption.Type.SIGN_KEY_IDENTITY,
SignKeyOption.Type.SIGN_KEY_IDENTITY_SHA1
).contains(packageSignOption.get().type())) {
// They request to sign the .pkg installer without
// the "--mac-installer-sign-identity" option,
// but didn't specify a signing option for the packaged app image.
@ -204,15 +207,61 @@ public class SigningPackageTest {
}
MacSign.ResolvedKeychain keychain() {
return SigningBase.StandardKeychain.MAIN.keychain();
return chooseKeychain(Stream.of(
appImageSignOption.stream(),
packageSignOption.stream()
).flatMap(x -> x).map(SignKeyOption::type).findFirst().orElseThrow()).keychain();
}
static List<TestSpec> testCases(boolean withUnicode) {
/**
* Types of test cases to skip.
*/
enum SkipTestCases {
/**
* Skip test cases with signing identities/key names with symbols outside of the
* ASCII codepage.
*/
SKIP_UNICODE,
/**
* Skip test cases in which the value of the "--mac-signing-key-user-name"
* option is the full signing identity name.
*/
SKIP_SIGN_KEY_USER_FULL_NAME,
/**
* Skip test cases in which the value of the "--mac-installer-sign-identity" or
* "--mac-app-image-sign-identity" option is the SHA1 digest of the signing
* certificate.
*/
SKIP_SIGN_KEY_IDENTITY_SHA1,
;
}
static List<TestSpec> minimalTestCases() {
return testCases(SkipTestCases.values());
}
static List<TestSpec> testCases(SkipTestCases... skipTestCases) {
final var skipTestCasesAsSet = Set.of(skipTestCases);
final var signIdentityTypes = Stream.of(SignKeyOption.Type.defaultValues()).filter(v -> {
switch (v) {
case SIGN_KEY_USER_FULL_NAME -> {
return !skipTestCasesAsSet.contains(SkipTestCases.SKIP_SIGN_KEY_USER_FULL_NAME);
}
case SIGN_KEY_IDENTITY_SHA1 -> {
return !skipTestCasesAsSet.contains(SkipTestCases.SKIP_SIGN_KEY_IDENTITY_SHA1);
}
default -> {
return true;
}
}
}).toList();
List<TestSpec> data = new ArrayList<>();
List<List<SigningBase.StandardCertificateRequest>> certRequestGroups;
if (withUnicode) {
if (!skipTestCasesAsSet.contains(SkipTestCases.SKIP_UNICODE)) {
certRequestGroups = List.of(
List.of(SigningBase.StandardCertificateRequest.CODESIGN, SigningBase.StandardCertificateRequest.PKG),
List.of(SigningBase.StandardCertificateRequest.CODESIGN_UNICODE, SigningBase.StandardCertificateRequest.PKG_UNICODE)
@ -224,19 +273,33 @@ public class SigningPackageTest {
}
for (var certRequests : certRequestGroups) {
for (var signIdentityType : SignKeyOption.Type.defaultValues()) {
var keychain = SigningBase.StandardKeychain.MAIN.keychain();
for (var signIdentityType : signIdentityTypes) {
if (signIdentityType == SignKeyOption.Type.SIGN_KEY_IMPLICIT
&& !SigningBase.StandardKeychain.SINGLE.contains(certRequests.getFirst())) {
// Skip invalid test case: the keychain for testing signing without
// an explicitly specified signing key option doesn't have this signing key.
break;
}
if (signIdentityType.passThrough() && !certRequests.contains(SigningBase.StandardCertificateRequest.CODESIGN)) {
// Using a pass-through signing option.
// Doesn't make sense to waste time on testing it with multiple certificates.
// Skip the test cases using non "default" certificate.
break;
}
var keychain = chooseKeychain(signIdentityType).keychain();
var appImageSignKeyOption = new SignKeyOption(signIdentityType, certRequests.getFirst(), keychain);
var pkgSignKeyOption = new SignKeyOption(signIdentityType, certRequests.getLast(), keychain);
switch (signIdentityType) {
case SIGN_KEY_IDENTITY -> {
case SIGN_KEY_IDENTITY, SIGN_KEY_IDENTITY_SHA1 -> {
// Use "--mac-installer-sign-identity" and "--mac-app-image-sign-identity" signing options.
// They allows to sign the packaged app image and the installer (.pkg) separately.
data.add(new TestSpec(Optional.of(appImageSignKeyOption), Optional.empty(), PackageType.MAC));
data.add(new TestSpec(Optional.empty(), Optional.of(pkgSignKeyOption), PackageType.MAC_PKG));
}
case SIGN_KEY_USER_SHORT_NAME -> {
case SIGN_KEY_USER_SHORT_NAME, SIGN_KEY_IMPLICIT -> {
// Use "--mac-signing-key-user-name" signing option with short user name or implicit signing option.
// It signs both the packaged app image and the installer (.pkg).
// Thus, if the installer is not signed, it can be used only with .dmg packaging.
@ -263,5 +326,13 @@ public class SigningPackageTest {
return data;
}
private static SigningBase.StandardKeychain chooseKeychain(SignKeyOption.Type signIdentityType) {
if (signIdentityType == SignKeyOption.Type.SIGN_KEY_IMPLICIT) {
return SigningBase.StandardKeychain.SINGLE;
} else {
return SigningBase.StandardKeychain.MAIN;
}
}
}
}

View File

@ -108,7 +108,7 @@ public class SigningPackageTwoStepTest {
appImageSignOption = Optional.empty();
}
for (var signPackage : SigningPackageTest.TestSpec.testCases(false)) {
for (var signPackage : SigningPackageTest.TestSpec.minimalTestCases()) {
data.add(new TwoStepsTestSpec(appImageSignOption, signPackage));
}
}

View File

@ -125,7 +125,7 @@ public class SigningRuntimeImagePackageTest {
runtimeSignOption = Optional.empty();
}
for (var signPackage : SigningPackageTest.TestSpec.testCases(false)) {
for (var signPackage : SigningPackageTest.TestSpec.minimalTestCases()) {
data.add(new RuntimeTestSpec(runtimeSignOption, runtimeType, signPackage));
}
}

View File

@ -26,7 +26,7 @@ import static java.util.stream.Collectors.toMap;
import static jdk.internal.util.OperatingSystem.LINUX;
import static jdk.internal.util.OperatingSystem.MACOS;
import static jdk.internal.util.OperatingSystem.WINDOWS;
import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import static jdk.jpackage.test.JPackageCommand.makeAdvice;
import static jdk.jpackage.test.JPackageCommand.makeError;
@ -45,6 +45,7 @@ import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.util.TokenReplace;
import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.Annotations.ParameterSupplier;
@ -53,14 +54,27 @@ import jdk.jpackage.test.CannedArgument;
import jdk.jpackage.test.CannedFormattedString;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.JPackageOutputValidator;
import jdk.jpackage.test.MacSign;
import jdk.jpackage.test.MacSign.CertificateRequest;
import jdk.jpackage.test.MacSign.CertificateType;
import jdk.jpackage.test.MacSign.KeychainWithCertsSpec;
import jdk.jpackage.test.MacSign.ResolvedKeychain;
import jdk.jpackage.test.MacSign.StandardCertificateNamePrefix;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.TKit;
import jdk.jpackage.test.mock.Script;
import jdk.jpackage.test.mock.VerbatimCommandMock;
import jdk.jpackage.test.stdmock.JPackageMockUtils;
import jdk.jpackage.test.stdmock.MacSignMockUtils;
/*
* @test
* @summary Test jpackage output for erroneous input
* @library /test/jdk/tools/jpackage/helpers
* @library /test/lib
* @build jdk.jpackage.test.*
* @build jdk.jpackage.test.stdmock.*
* @build jdk.test.lib.security.CertificateBuilder
* @compile -Xlint:all -Werror ErrorTest.java
* @run main/othervm/timeout=720 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=ErrorTest
@ -71,7 +85,10 @@ import jdk.jpackage.test.TKit;
* @test
* @summary Test jpackage output for erroneous input
* @library /test/jdk/tools/jpackage/helpers
* @library /test/lib
* @build jdk.jpackage.test.*
* @build jdk.jpackage.test.stdmock.*
* @build jdk.test.lib.security.CertificateBuilder
* @compile -Xlint:all -Werror ErrorTest.java
* @run main/othervm/timeout=720 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=ErrorTest
@ -81,10 +98,10 @@ import jdk.jpackage.test.TKit;
public final class ErrorTest {
enum Token {
JAVA_HOME(cmd -> {
JAVA_HOME(() -> {
return System.getProperty("java.home");
}),
APP_IMAGE(cmd -> {
APP_IMAGE(() -> {
final var appImageRoot = TKit.createTempDirectory("appimage");
final var appImageCmd = JPackageCommand.helloAppImage()
@ -92,28 +109,44 @@ public final class ErrorTest {
appImageCmd.execute();
return appImageCmd.outputBundle().toString();
return appImageCmd.outputBundle();
}),
INVALID_MAC_RUNTIME_BUNDLE(toFunction(cmd -> {
APP_IMAGE_WITH_SHORT_NAME(() -> {
final var appImageRoot = TKit.createTempDirectory("appimage");
final var appImageCmd = JPackageCommand.helloAppImage()
.setFakeRuntime().setArgumentValue("--dest", appImageRoot);
// Let jpackage pick the name from the main class (Hello). It qualifies as the "short" name.
appImageCmd.removeArgumentWithValue("--name");
appImageCmd.execute();
return appImageCmd.outputBundle();
}),
INVALID_MAC_RUNTIME_BUNDLE(toSupplier(() -> {
// Has "Contents/MacOS/libjli.dylib", but missing "Contents/Home/lib/libjli.dylib".
final Path root = TKit.createTempDirectory("mac-invalid-runtime-bundle");
Files.createDirectories(root.resolve("Contents/Home"));
Files.createFile(root.resolve("Contents/Info.plist"));
Files.createDirectories(root.resolve("Contents/MacOS"));
Files.createFile(root.resolve("Contents/MacOS/libjli.dylib"));
return root.toString();
return root;
})),
INVALID_MAC_RUNTIME_IMAGE(toFunction(cmd -> {
INVALID_MAC_RUNTIME_IMAGE(toSupplier(() -> {
// Has some files in the "lib" subdirectory, but doesn't have the "lib/libjli.dylib" file.
final Path root = TKit.createTempDirectory("mac-invalid-runtime-image");
Files.createDirectories(root.resolve("lib"));
Files.createFile(root.resolve("lib/foo"));
return root.toString();
return root;
})),
EMPTY_DIR(toFunction(cmd -> {
EMPTY_DIR(() -> {
return TKit.createTempDirectory("empty-dir");
})),
}),
ADD_LAUNCHER_PROPERTY_FILE,
EMPTY_KEYCHAIN,
KEYCHAIN_WITH_APP_IMAGE_CERT,
KEYCHAIN_WITH_PKG_CERT,
;
private Token() {
@ -124,6 +157,12 @@ public final class ErrorTest {
this.valueSupplier = Optional.of(valueSupplier);
}
private Token(Supplier<Object> valueSupplier) {
this(_ -> {
return valueSupplier.get();
});
}
String token() {
return makeToken(name());
}
@ -558,7 +597,7 @@ public final class ErrorTest {
}
public static Collection<Object[]> invalidAppVersion() {
return fromTestSpecBuilders(Stream.of(
return toTestArgs(Stream.of(
// Invalid app version. Just cover all different error messages.
// Extensive testing of invalid version strings is done in DottedVersionTest unit test.
testSpec().addArgs("--app-version", "").error("error.version-string-empty"),
@ -601,7 +640,7 @@ public final class ErrorTest {
argsStream = Stream.concat(argsStream, Stream.of(List.of("--win-console")));
}
return fromTestSpecBuilders(argsStream.map(args -> {
return toTestArgs(argsStream.map(args -> {
var builder = testSpec().noAppDesc().nativeType()
.addArgs("--runtime-image", Token.JAVA_HOME.token())
.addArgs(args);
@ -629,7 +668,7 @@ public final class ErrorTest {
}
public static Collection<Object[]> testAdditionLaunchers() {
return fromTestSpecBuilders(Stream.of(
return toTestArgs(Stream.of(
testSpec().addArgs("--add-launcher", Token.ADD_LAUNCHER_PROPERTY_FILE.token())
.error("error.parameter-add-launcher-malformed", Token.ADD_LAUNCHER_PROPERTY_FILE, "--add-launcher"),
testSpec().removeArgs("--name").addArgs("--name", "foo", "--add-launcher", "foo=" + Token.ADD_LAUNCHER_PROPERTY_FILE.token())
@ -637,6 +676,184 @@ public final class ErrorTest {
));
}
@Test(ifOS = MACOS)
@ParameterSupplier
@ParameterSupplier("testMacPkgSignWithoutIdentity")
public static void testMacSignWithoutIdentity(TestSpec spec) {
// The test called JPackage Command.useToolProviderBy Default(),
// which alters global variables in the test library,
// so run the test case with a new global state to isolate the alteration of the globals.
TKit.withNewState(() -> {
testMacSignWithoutIdentityWithNewTKitState(spec);
});
}
private static void testMacSignWithoutIdentityWithNewTKitState(TestSpec spec) {
final Token keychainToken = spec.expectedMessages().stream().flatMap(cannedStr -> {
return Stream.of(cannedStr.args()).filter(Token.class::isInstance).map(Token.class::cast).filter(token -> {
switch (token) {
case EMPTY_KEYCHAIN, KEYCHAIN_WITH_APP_IMAGE_CERT, KEYCHAIN_WITH_PKG_CERT -> {
return true;
}
default -> {
return false;
}
}
});
}).distinct().reduce((a, b) -> {
throw new IllegalStateException(String.format(
"Error messages %s reference multiple keychains: %s and %s", spec.expectedMessages(), a, b));
}).orElseThrow();
final ResolvedKeychain keychain;
switch (keychainToken) {
case EMPTY_KEYCHAIN -> {
keychain = new ResolvedKeychain(new KeychainWithCertsSpec(MacSign.createEmptyKeychain(), List.of()));
}
case KEYCHAIN_WITH_APP_IMAGE_CERT, KEYCHAIN_WITH_PKG_CERT -> {
CertificateType existingCertType;
switch (keychainToken) {
case KEYCHAIN_WITH_APP_IMAGE_CERT -> {
existingCertType = CertificateType.CODE_SIGN;
}
case KEYCHAIN_WITH_PKG_CERT -> {
existingCertType = CertificateType.INSTALLER;
}
default -> {
throw new AssertionError();
}
}
keychain = Stream.of(SignEnvMock.SingleCertificateKeychain.values()).filter(k -> {
return k.certificateType() == existingCertType;
}).findFirst().orElseThrow().keychain();
var script = Script.build()
// Disable the mutation making mocks "run once".
.commandMockBuilderMutator(null)
// Replace "/usr/bin/security" with the mock bound to the keychain mock.
.map(MacSignMockUtils.securityMock(SignEnvMock.VALUE))
// Don't mock other external commands.
.use(VerbatimCommandMock.INSTANCE)
.createLoop();
// Create jpackage tool provider using the /usr/bin/security mock.
var jpackage = JPackageMockUtils.createJPackageToolProvider(OperatingSystem.MACOS, script);
// Override the default jpackage tool provider with the one using the /usr/bin/security mock.
JPackageCommand.useToolProviderByDefault(jpackage);
}
default -> {
throw new AssertionError();
}
}
MacSign.withKeychain(_ -> {
spec.mapExpectedMessages(cannedStr -> {
return cannedStr.mapArgs(arg -> {
switch (arg) {
case StandardCertificateNamePrefix certPrefix -> {
return certPrefix.value();
}
case Token _ -> {
return keychain.name();
}
default -> {
return arg;
}
}
});
}).test(Map.of(keychainToken, _ -> keychain.name()));
}, keychain);
}
public static Collection<Object[]> testMacSignWithoutIdentity() {
final List<TestSpec> testCases = new ArrayList<>();
final var signArgs = List.of("--mac-sign", "--mac-signing-keychain", Token.EMPTY_KEYCHAIN.token());
final var appImageArgs = List.of("--app-image", Token.APP_IMAGE_WITH_SHORT_NAME.token());
for (var withAppImage : List.of(true, false)) {
var builder = testSpec();
if (withAppImage) {
builder.noAppDesc().addArgs(appImageArgs);
}
builder.addArgs(signArgs);
for (var type: List.of(PackageType.IMAGE, PackageType.MAC_PKG, PackageType.MAC_DMG)) {
builder.setMessages().error("error.cert.not.found",
MacSign.StandardCertificateNamePrefix.CODE_SIGN, Token.EMPTY_KEYCHAIN);
switch (type) {
case MAC_PKG -> {
// jpackage must report two errors:
// 1. It can't find signing identity to sign the app image
// 2. It can't find signing identity to sign the PKG installer
builder.error("error.cert.not.found",
MacSign.StandardCertificateNamePrefix.INSTALLER, Token.EMPTY_KEYCHAIN);
}
default -> {
// NOP
}
}
var testSpec = builder.type(type).create();
testCases.add(testSpec);
}
}
return toTestArgs(testCases);
}
public static Collection<Object[]> testMacPkgSignWithoutIdentity() {
final List<TestSpec.Builder> testCases = new ArrayList<>();
final var appImageArgs = List.of("--app-image", Token.APP_IMAGE_WITH_SHORT_NAME.token());
for (var withAppImage : List.of(true, false)) {
for (var existingCertType : CertificateType.values()) {
Token keychain;
StandardCertificateNamePrefix missingCertificateNamePrefix;
switch (existingCertType) {
case INSTALLER -> {
keychain = Token.KEYCHAIN_WITH_PKG_CERT;
missingCertificateNamePrefix = StandardCertificateNamePrefix.CODE_SIGN;
}
case CODE_SIGN -> {
keychain = Token.KEYCHAIN_WITH_APP_IMAGE_CERT;
missingCertificateNamePrefix = StandardCertificateNamePrefix.INSTALLER;
}
default -> {
throw new AssertionError();
}
}
var builder = testSpec()
.type(PackageType.MAC_PKG)
.addArgs("--mac-sign", "--mac-signing-keychain", keychain.token())
.error("error.cert.not.found", missingCertificateNamePrefix, keychain);
if (withAppImage) {
builder.noAppDesc().addArgs(appImageArgs);
} else {
/*
* Use shorter name to avoid
*
* [03:08:55.623] --mac-package-name is set to 'MacSignWithoutIdentityErrorTest', which is longer than 16 characters. For a better Mac experience consider shortening it.
*
* in the output.
* The same idea is behind using the "APP_IMAGE_WITH_SHORT_NAME" token
* instead of the "APP_IMAGE" for the predefined app image.
*/
builder.removeArgs("--name");
}
testCases.add(builder);
}
}
return toTestArgs(testCases);
}
@Test
@ParameterSupplier("invalidNames")
public static void testInvalidAppName(InvalidName name) {
@ -1007,8 +1224,14 @@ public final class ErrorTest {
);
}
private static <T> Collection<Object[]> toTestArgs(Stream<T> stream) {
return stream.filter(v -> {
private static Collection<Object[]> toTestArgs(Stream<?> stream) {
return stream.map(v -> {
if (v instanceof TestSpec.Builder builder) {
return builder.create();
} else {
return v;
}
}).filter(v -> {
if (v instanceof TestSpec ts) {
return ts.isSupported();
} else {
@ -1019,8 +1242,8 @@ public final class ErrorTest {
}).toList();
}
private static Collection<Object[]> fromTestSpecBuilders(Stream<TestSpec.Builder> stream) {
return toTestArgs(stream.map(TestSpec.Builder::create));
private static Collection<Object[]> toTestArgs(Collection<?> col) {
return toTestArgs(col.stream());
}
private static String adjustTextStreamVerifierArg(String str) {
@ -1028,4 +1251,40 @@ public final class ErrorTest {
}
private static final Pattern LINE_SEP_REGEXP = Pattern.compile("\\R");
private final class SignEnvMock {
enum SingleCertificateKeychain {
FOO(CertificateType.CODE_SIGN),
BAR(CertificateType.INSTALLER),
;
SingleCertificateKeychain(CertificateType certificateType) {
this.keychain = KeychainWithCertsSpec.build()
.name(name().toLowerCase() + ".keychain")
.addCert(CertificateRequest.build()
.userName(name().toLowerCase())
.type(Objects.requireNonNull(certificateType)))
.create();
}
static List<KeychainWithCertsSpec> signingEnv() {
return Stream.of(values()).map(v -> {
return v.keychain;
}).toList();
}
CertificateType certificateType() {
return keychain.certificateRequests().getFirst().type();
}
ResolvedKeychain keychain() {
return new ResolvedKeychain(keychain).toMock(VALUE.env());
}
private final KeychainWithCertsSpec keychain;
}
static final MacSignMockUtils.SignEnv VALUE = new MacSignMockUtils.SignEnv(SingleCertificateKeychain.signingEnv());
}
}