mirror of
https://github.com/openjdk/jdk.git
synced 2026-03-01 19:50:05 +00:00
8371438: jpackage should handle the case when "--mac-sign" is specified without signing identity options
Reviewed-by: almatvee
This commit is contained in:
parent
526228ca3f
commit
4a6de12b3a
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 -> {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user