mirror of
https://github.com/JetBrains/JetBrainsRuntime.git
synced 2025-12-06 09:29:38 +01:00
8308042: [macos] Developer ID Application Certificate not picked up by jpackage if it contains UNICODE characters
Reviewed-by: asemenyuk
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2012, 2023, 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
|
||||
@@ -62,13 +62,13 @@ public class MacAppBundler extends AppImageBundler {
|
||||
String keychain = SIGNING_KEYCHAIN.fetchFrom(params);
|
||||
String result = null;
|
||||
if (APP_STORE.fetchFrom(params)) {
|
||||
result = MacBaseInstallerBundler.findKey(
|
||||
result = MacCertificate.findCertificateKey(
|
||||
"3rd Party Mac Developer Application: ",
|
||||
user, keychain);
|
||||
}
|
||||
// if either not signing for app store or couldn't find
|
||||
if (result == null) {
|
||||
result = MacBaseInstallerBundler.findKey(
|
||||
result = MacCertificate.findCertificateKey(
|
||||
"Developer ID Application: ", user, keychain);
|
||||
}
|
||||
|
||||
|
||||
@@ -207,47 +207,5 @@ public abstract class MacBaseInstallerBundler extends AbstractBundler {
|
||||
return "INSTALLER";
|
||||
}
|
||||
|
||||
public static String findKey(String keyPrefix, String teamName, String keychainName) {
|
||||
|
||||
boolean useAsIs = teamName.startsWith(keyPrefix)
|
||||
|| teamName.startsWith("Developer ID")
|
||||
|| teamName.startsWith("3rd Party Mac");
|
||||
|
||||
String key = (useAsIs) ? teamName : (keyPrefix + teamName);
|
||||
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
PrintStream ps = new PrintStream(baos)) {
|
||||
List<String> searchOptions = new ArrayList<>();
|
||||
searchOptions.add("/usr/bin/security");
|
||||
searchOptions.add("find-certificate");
|
||||
searchOptions.add("-c");
|
||||
searchOptions.add(key);
|
||||
searchOptions.add("-a");
|
||||
if (keychainName != null && !keychainName.isEmpty()) {
|
||||
searchOptions.add(keychainName);
|
||||
}
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(searchOptions);
|
||||
|
||||
IOUtils.exec(pb, false, ps);
|
||||
Pattern p = Pattern.compile("\"alis\"<blob>=\"([^\"]+)\"");
|
||||
Matcher m = p.matcher(baos.toString());
|
||||
if (!m.find()) {
|
||||
Log.error(MessageFormat.format(I18N.getString(
|
||||
"error.cert.not.found"), key, keychainName));
|
||||
return null;
|
||||
}
|
||||
String matchedKey = m.group(1);
|
||||
if (m.find()) {
|
||||
Log.error(MessageFormat.format(I18N.getString(
|
||||
"error.multiple.certs.found"), key, keychainName));
|
||||
}
|
||||
return matchedKey;
|
||||
} catch (IOException ioe) {
|
||||
Log.verbose(ioe);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private final Bundler appImageBundler;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2016, 2020, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2016, 2023, 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
|
||||
@@ -33,6 +33,7 @@ import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.DateFormat;
|
||||
import java.text.MessageFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
@@ -40,6 +41,9 @@ import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.HexFormat;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class MacCertificate {
|
||||
private final String certificate;
|
||||
@@ -52,35 +56,219 @@ public final class MacCertificate {
|
||||
return verifyCertificate(this.certificate);
|
||||
}
|
||||
|
||||
private static Path findCertificate(String certificate) {
|
||||
Path result = null;
|
||||
public static String findCertificateKey(String keyPrefix, String teamName,
|
||||
String keychainName) {
|
||||
|
||||
String matchedKey = null;
|
||||
boolean useAsIs = (keyPrefix == null)
|
||||
|| teamName.startsWith(keyPrefix)
|
||||
|| teamName.startsWith("Developer ID")
|
||||
|| teamName.startsWith("3rd Party Mac");
|
||||
|
||||
String name = (useAsIs) ? teamName : (keyPrefix + teamName);
|
||||
|
||||
String output = getFindCertificateOutput(name, keychainName);
|
||||
if (output == null) {
|
||||
Log.error(MessageFormat.format(I18N.getString(
|
||||
"error.cert.not.found"), name, keychainName));
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check and warn user if multiple certificates found
|
||||
// We will use different regex to count certificates.
|
||||
// ASCII case: "alis"<blob>="NAME"
|
||||
// UNICODE case: "alis"<blob>=0xSOMEHEXDIGITS "NAME (\SOMEDIGITS)"
|
||||
// In UNICODE case name will contain octal sequence representing UTF-8
|
||||
// characters.
|
||||
// Just look for at least two '"alis"<blob>'.
|
||||
Pattern p = Pattern.compile("\"alis\"<blob>");
|
||||
Matcher m = p.matcher(output);
|
||||
if (m.find() && m.find()) {
|
||||
Log.error(MessageFormat.format(I18N.getString(
|
||||
"error.multiple.certs.found"), name, keychainName));
|
||||
}
|
||||
|
||||
// Try to get ASCII only certificate first. This aproach only works
|
||||
// if certificate name has ASCII only characters in name. For certificates
|
||||
// with UNICODE characters in name we will use combination of "security"
|
||||
// and "openssl". We keeping ASCII only aproach to avoid regressions and
|
||||
// it works for many use cases.
|
||||
p = Pattern.compile("\"alis\"<blob>=\"([^\"]+)\"");
|
||||
m = p.matcher(output);
|
||||
if (m.find()) {
|
||||
matchedKey = m.group(1);;
|
||||
}
|
||||
|
||||
// Maybe it has UNICODE characters in name. In this case use "security"
|
||||
// and "openssl" to exctract name. We cannot use just "security", since
|
||||
// name can be truncated.
|
||||
if (matchedKey == null) {
|
||||
Path file = null;
|
||||
try {
|
||||
file = getFindCertificateOutputPEM(name, keychainName);
|
||||
if (file != null) {
|
||||
matchedKey = findCertificateSubject(
|
||||
file.toFile().getCanonicalPath());
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Log.verbose(ioe);
|
||||
} finally {
|
||||
try {
|
||||
Files.deleteIfExists(file);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedKey == null) {
|
||||
Log.error(MessageFormat.format(I18N.getString(
|
||||
"error.cert.not.found"), name, keychainName));
|
||||
}
|
||||
|
||||
return matchedKey;
|
||||
}
|
||||
|
||||
private static String getFindCertificateOutput(String name,
|
||||
String keychainName) {
|
||||
try (ByteArrayOutputStream baos = getFindCertificateOutput(name,
|
||||
keychainName,
|
||||
false)) {
|
||||
if (baos != null) {
|
||||
return baos.toString();
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Log.verbose(ioe);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Path getFindCertificateOutputPEM(String name,
|
||||
String keychainName) {
|
||||
Path output = null;
|
||||
try (ByteArrayOutputStream baos = getFindCertificateOutput(name,
|
||||
keychainName,
|
||||
true)) {
|
||||
if (baos != null) {
|
||||
output = Files.createTempFile("tempfile", ".tmp");
|
||||
Files.copy(new ByteArrayInputStream(baos.toByteArray()),
|
||||
output, StandardCopyOption.REPLACE_EXISTING);
|
||||
return output;
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Log.verbose(ioe);
|
||||
try {
|
||||
Files.deleteIfExists(output);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ByteArrayOutputStream getFindCertificateOutput(String name,
|
||||
String keychainName,
|
||||
boolean isPEMFormat) {
|
||||
List<String> args = new ArrayList<>();
|
||||
args.add("/usr/bin/security");
|
||||
args.add("find-certificate");
|
||||
args.add("-c");
|
||||
args.add(certificate);
|
||||
args.add(name);
|
||||
args.add("-a");
|
||||
args.add("-p");
|
||||
if (isPEMFormat) {
|
||||
args.add("-p");
|
||||
}
|
||||
if (keychainName != null && !keychainName.isEmpty()) {
|
||||
args.add(keychainName);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try (PrintStream ps = new PrintStream(baos)) {
|
||||
ProcessBuilder pb = new ProcessBuilder(args);
|
||||
IOUtils.exec(pb, false, ps);
|
||||
return baos;
|
||||
} catch (IOException ioe) {
|
||||
Log.verbose(ioe);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String findCertificateSubject(String filename) {
|
||||
String result = null;
|
||||
|
||||
List<String> args = new ArrayList<>();
|
||||
args.add("/usr/bin/openssl");
|
||||
args.add("x509");
|
||||
args.add("-noout");
|
||||
args.add("-subject");
|
||||
args.add("-in");
|
||||
args.add(filename);
|
||||
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
PrintStream ps = new PrintStream(baos)) {
|
||||
ProcessBuilder security = new ProcessBuilder(args);
|
||||
IOUtils.exec(security, false, ps);
|
||||
|
||||
Path output = Files.createTempFile("tempfile", ".tmp");
|
||||
|
||||
Files.copy(new ByteArrayInputStream(baos.toByteArray()),
|
||||
output, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
result = output;
|
||||
String output = baos.toString().strip();
|
||||
// Example output:
|
||||
// subject= /UID=ABCDABCD/CN=jpackage.openjdk.java.net (\xC3\xB6) (ABCDABCD)/C=US
|
||||
// We need 'CN' value
|
||||
String [] pairs = output.split("/");
|
||||
for (String pair : pairs) {
|
||||
if (pair.startsWith("CN=")) {
|
||||
result = pair.substring(3);
|
||||
// Convert escaped UTF-8 code points to characters
|
||||
result = convertHexToChar(result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
Log.verbose(ex);
|
||||
}
|
||||
catch (IOException ignored) {}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Date findCertificateDate(String filename) {
|
||||
// Certificate name with Unicode will be:
|
||||
// Developer ID Application: jpackage.openjdk.java.net (\xHH\xHH)
|
||||
// Convert UTF-8 code points '\xHH\xHH' to character.
|
||||
private static String convertHexToChar(String input) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (!input.contains("\\x")) {
|
||||
return input;
|
||||
}
|
||||
|
||||
StringBuilder output = new StringBuilder();
|
||||
try {
|
||||
int len = input.length();
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (input.codePointAt(i) == '\\' &&
|
||||
(i + 8) <= len &&
|
||||
input.codePointAt(i + 1) == 'x' &&
|
||||
input.codePointAt(i + 4) == '\\' &&
|
||||
input.codePointAt(i + 5) == 'x') {
|
||||
// We found '\xHH\xHH'
|
||||
// HEX code points to byte array
|
||||
byte [] bytes = HexFormat.of().parseHex(
|
||||
input.substring(i + 2, i + 4) + input.substring(i + 6, i + 8));
|
||||
// Byte array with UTF-8 code points to character
|
||||
output.append(new String(bytes, "UTF-8"));
|
||||
i += 7; // Skip '\xHH\xHH'
|
||||
} else {
|
||||
output.appendCodePoint(input.codePointAt(i));
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log.verbose(ex);
|
||||
// We will consider any excpetions during conversion as
|
||||
// certificate not found.
|
||||
return null;
|
||||
}
|
||||
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
private Date findCertificateDate(String filename) {
|
||||
Date result = null;
|
||||
|
||||
List<String> args = new ArrayList<>();
|
||||
@@ -107,7 +295,7 @@ public final class MacCertificate {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean verifyCertificate(String certificate) {
|
||||
private boolean verifyCertificate(String certificate) {
|
||||
boolean result = false;
|
||||
|
||||
try {
|
||||
@@ -115,16 +303,15 @@ public final class MacCertificate {
|
||||
Date certificateDate = null;
|
||||
|
||||
try {
|
||||
file = findCertificate(certificate);
|
||||
file = getFindCertificateOutputPEM(certificate, null);
|
||||
|
||||
if (file != null) {
|
||||
certificateDate = findCertificateDate(
|
||||
file.toFile().getCanonicalPath());
|
||||
}
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
if (file != null) {
|
||||
Files.delete(file);
|
||||
Files.deleteIfExists(file);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,13 +110,13 @@ public class MacPkgBundler extends MacBaseInstallerBundler {
|
||||
String keychain = SIGNING_KEYCHAIN.fetchFrom(params);
|
||||
String result = null;
|
||||
if (APP_STORE.fetchFrom(params)) {
|
||||
result = MacBaseInstallerBundler.findKey(
|
||||
result = MacCertificate.findCertificateKey(
|
||||
"3rd Party Mac Developer Installer: ",
|
||||
user, keychain);
|
||||
}
|
||||
// if either not signing for app store or couldn't find
|
||||
if (result == null) {
|
||||
result = MacBaseInstallerBundler.findKey(
|
||||
result = MacCertificate.findCertificateKey(
|
||||
"Developer ID Installer: ", user, keychain);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2019, 2023, 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
|
||||
@@ -61,31 +61,37 @@ import jdk.jpackage.test.AdditionalLauncher;
|
||||
public class SigningAppImageTest {
|
||||
|
||||
@Test
|
||||
@Parameter("true")
|
||||
@Parameter("false")
|
||||
public void test(boolean doSign) throws Exception {
|
||||
SigningCheck.checkCertificates();
|
||||
@Parameter({"true", "0"}) // ({"sign or not", "certificate index"})
|
||||
@Parameter({"true", "1"})
|
||||
@Parameter({"false", "-1"})
|
||||
public void test(String... testArgs) throws Exception {
|
||||
boolean doSign = Boolean.parseBoolean(testArgs[0]);
|
||||
int certIndex = Integer.parseInt(testArgs[1]);
|
||||
|
||||
SigningCheck.checkCertificates(certIndex);
|
||||
|
||||
JPackageCommand cmd = JPackageCommand.helloAppImage();
|
||||
if (doSign) {
|
||||
cmd.addArguments("--mac-sign", "--mac-signing-key-user-name",
|
||||
SigningBase.DEV_NAME, "--mac-signing-keychain",
|
||||
SigningBase.KEYCHAIN);
|
||||
cmd.addArguments("--mac-sign",
|
||||
"--mac-signing-key-user-name",
|
||||
SigningBase.getDevName(certIndex),
|
||||
"--mac-signing-keychain",
|
||||
SigningBase.getKeyChain());
|
||||
}
|
||||
AdditionalLauncher testAL = new AdditionalLauncher("testAL");
|
||||
testAL.applyTo(cmd);
|
||||
cmd.executeAndAssertHelloAppImageCreated();
|
||||
|
||||
Path launcherPath = cmd.appLauncherPath();
|
||||
SigningBase.verifyCodesign(launcherPath, doSign);
|
||||
SigningBase.verifyCodesign(launcherPath, doSign, certIndex);
|
||||
|
||||
Path testALPath = launcherPath.getParent().resolve("testAL");
|
||||
SigningBase.verifyCodesign(testALPath, doSign);
|
||||
SigningBase.verifyCodesign(testALPath, doSign, certIndex);
|
||||
|
||||
Path appImage = cmd.outputBundle();
|
||||
SigningBase.verifyCodesign(appImage, doSign);
|
||||
SigningBase.verifyCodesign(appImage, doSign, certIndex);
|
||||
if (doSign) {
|
||||
SigningBase.verifySpctl(appImage, "exec");
|
||||
SigningBase.verifySpctl(appImage, "exec", certIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2022, 2023, 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
|
||||
@@ -65,7 +65,7 @@ public class SigningAppImageTwoStepsTest {
|
||||
@Parameter("true")
|
||||
@Parameter("false")
|
||||
public void test(boolean signAppImage) throws Exception {
|
||||
SigningCheck.checkCertificates();
|
||||
SigningCheck.checkCertificates(SigningBase.DEFAULT_INDEX);
|
||||
|
||||
Path appimageOutput = TKit.createTempDirectory("appimage");
|
||||
|
||||
@@ -75,9 +75,11 @@ public class SigningAppImageTwoStepsTest {
|
||||
JPackageCommand appImageCmd = JPackageCommand.helloAppImage()
|
||||
.setArgumentValue("--dest", appimageOutput);
|
||||
if (signAppImage) {
|
||||
appImageCmd.addArguments("--mac-sign", "--mac-signing-key-user-name",
|
||||
SigningBase.DEV_NAME, "--mac-signing-keychain",
|
||||
SigningBase.KEYCHAIN);
|
||||
appImageCmd.addArguments("--mac-sign",
|
||||
"--mac-signing-key-user-name",
|
||||
SigningBase.getDevName(SigningBase.DEFAULT_INDEX),
|
||||
"--mac-signing-keychain",
|
||||
SigningBase.getKeyChain());
|
||||
}
|
||||
|
||||
// Add addtional launcher
|
||||
@@ -95,8 +97,9 @@ public class SigningAppImageTwoStepsTest {
|
||||
cmd.setPackageType(PackageType.IMAGE)
|
||||
.addArguments("--app-image", appImageCmd.outputBundle().toAbsolutePath())
|
||||
.addArguments("--mac-sign")
|
||||
.addArguments("--mac-signing-key-user-name", SigningBase.DEV_NAME)
|
||||
.addArguments("--mac-signing-keychain", SigningBase.KEYCHAIN);
|
||||
.addArguments("--mac-signing-key-user-name",
|
||||
SigningBase.getDevName(SigningBase.DEFAULT_INDEX))
|
||||
.addArguments("--mac-signing-keychain", SigningBase.getKeyChain());
|
||||
cmd.executeAndAssertImageCreated();
|
||||
|
||||
// Should be signed app image
|
||||
|
||||
@@ -72,8 +72,8 @@ public class SigningPackageFromTwoStepAppImageTest {
|
||||
}
|
||||
|
||||
Path outputBundle = cmd.outputBundle();
|
||||
SigningBase.verifyPkgutil(outputBundle);
|
||||
SigningBase.verifySpctl(outputBundle, "install");
|
||||
SigningBase.verifyPkgutil(outputBundle, SigningBase.DEFAULT_INDEX);
|
||||
SigningBase.verifySpctl(outputBundle, "install", SigningBase.DEFAULT_INDEX);
|
||||
}
|
||||
|
||||
private static void verifyDMG(JPackageCommand cmd) {
|
||||
@@ -89,9 +89,9 @@ public class SigningPackageFromTwoStepAppImageTest {
|
||||
if (dmgImage.endsWith(cmd.name() + ".app")) {
|
||||
Path launcherPath = ApplicationLayout.platformAppImage()
|
||||
.resolveAt(dmgImage).launchersDirectory().resolve(cmd.name());
|
||||
SigningBase.verifyCodesign(launcherPath, true);
|
||||
SigningBase.verifyCodesign(dmgImage, true);
|
||||
SigningBase.verifySpctl(dmgImage, "exec");
|
||||
SigningBase.verifyCodesign(launcherPath, true, SigningBase.DEFAULT_INDEX);
|
||||
SigningBase.verifyCodesign(dmgImage, true, SigningBase.DEFAULT_INDEX);
|
||||
SigningBase.verifySpctl(dmgImage, "exec", SigningBase.DEFAULT_INDEX);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -100,7 +100,7 @@ public class SigningPackageFromTwoStepAppImageTest {
|
||||
@Parameter("true")
|
||||
@Parameter("false")
|
||||
public static void test(boolean signAppImage) throws Exception {
|
||||
SigningCheck.checkCertificates();
|
||||
SigningCheck.checkCertificates(SigningBase.DEFAULT_INDEX);
|
||||
|
||||
Path appimageOutput = TKit.createTempDirectory("appimage");
|
||||
|
||||
@@ -111,8 +111,8 @@ public class SigningPackageFromTwoStepAppImageTest {
|
||||
.setArgumentValue("--dest", appimageOutput);
|
||||
if (signAppImage) {
|
||||
appImageCmd.addArguments("--mac-sign", "--mac-signing-key-user-name",
|
||||
SigningBase.DEV_NAME, "--mac-signing-keychain",
|
||||
SigningBase.KEYCHAIN);
|
||||
SigningBase.getDevName(SigningBase.DEFAULT_INDEX),
|
||||
"--mac-signing-keychain", SigningBase.getKeyChain());
|
||||
}
|
||||
|
||||
// Generate app image
|
||||
@@ -126,8 +126,9 @@ public class SigningPackageFromTwoStepAppImageTest {
|
||||
appImageSignedCmd.setPackageType(PackageType.IMAGE)
|
||||
.addArguments("--app-image", appImageCmd.outputBundle().toAbsolutePath())
|
||||
.addArguments("--mac-sign")
|
||||
.addArguments("--mac-signing-key-user-name", SigningBase.DEV_NAME)
|
||||
.addArguments("--mac-signing-keychain", SigningBase.KEYCHAIN);
|
||||
.addArguments("--mac-signing-key-user-name",
|
||||
SigningBase.getDevName(SigningBase.DEFAULT_INDEX))
|
||||
.addArguments("--mac-signing-keychain", SigningBase.getKeyChain());
|
||||
appImageSignedCmd.executeAndAssertImageCreated();
|
||||
|
||||
// Should be signed app image
|
||||
@@ -141,9 +142,9 @@ public class SigningPackageFromTwoStepAppImageTest {
|
||||
if (signAppImage) {
|
||||
cmd.addArguments("--mac-sign",
|
||||
"--mac-signing-key-user-name",
|
||||
SigningBase.DEV_NAME,
|
||||
SigningBase.getDevName(SigningBase.DEFAULT_INDEX),
|
||||
"--mac-signing-keychain",
|
||||
SigningBase.KEYCHAIN);
|
||||
SigningBase.getKeyChain());
|
||||
}
|
||||
})
|
||||
.forTypes(PackageType.MAC_PKG)
|
||||
|
||||
@@ -28,6 +28,7 @@ import jdk.jpackage.test.PackageTest;
|
||||
import jdk.jpackage.test.PackageType;
|
||||
import jdk.jpackage.test.MacHelper;
|
||||
import jdk.jpackage.test.Annotations.Test;
|
||||
import jdk.jpackage.test.Annotations.Parameter;
|
||||
|
||||
/**
|
||||
* Tests generation of dmg and pkg with --mac-sign and related arguments.
|
||||
@@ -65,8 +66,8 @@ public class SigningPackageTest {
|
||||
|
||||
private static void verifyPKG(JPackageCommand cmd) {
|
||||
Path outputBundle = cmd.outputBundle();
|
||||
SigningBase.verifyPkgutil(outputBundle);
|
||||
SigningBase.verifySpctl(outputBundle, "install");
|
||||
SigningBase.verifyPkgutil(outputBundle, getCertIndex(cmd));
|
||||
SigningBase.verifySpctl(outputBundle, "install", getCertIndex(cmd));
|
||||
}
|
||||
|
||||
private static void verifyDMG(JPackageCommand cmd) {
|
||||
@@ -81,24 +82,31 @@ public class SigningPackageTest {
|
||||
// We will be called with all folders in DMG since JDK-8263155, but
|
||||
// we only need to verify app.
|
||||
if (dmgImage.endsWith(cmd.name() + ".app")) {
|
||||
SigningBase.verifyCodesign(launcherPath, true);
|
||||
SigningBase.verifyCodesign(dmgImage, true);
|
||||
SigningBase.verifySpctl(dmgImage, "exec");
|
||||
SigningBase.verifyCodesign(launcherPath, true, getCertIndex(cmd));
|
||||
SigningBase.verifyCodesign(dmgImage, true, getCertIndex(cmd));
|
||||
SigningBase.verifySpctl(dmgImage, "exec", getCertIndex(cmd));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static int getCertIndex(JPackageCommand cmd) {
|
||||
String devName = cmd.getArgumentValue("--mac-signing-key-user-name");
|
||||
return SigningBase.getDevNameIndex(devName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public static void test() throws Exception {
|
||||
SigningCheck.checkCertificates();
|
||||
@Parameter("0")
|
||||
@Parameter("1")
|
||||
public static void test(int certIndex) throws Exception {
|
||||
SigningCheck.checkCertificates(certIndex);
|
||||
|
||||
new PackageTest()
|
||||
.configureHelloApp()
|
||||
.forTypes(PackageType.MAC)
|
||||
.addInitializer(cmd -> {
|
||||
cmd.addArguments("--mac-sign",
|
||||
"--mac-signing-key-user-name", SigningBase.DEV_NAME,
|
||||
"--mac-signing-keychain", SigningBase.KEYCHAIN);
|
||||
"--mac-signing-key-user-name", SigningBase.getDevName(certIndex),
|
||||
"--mac-signing-keychain", SigningBase.getKeyChain());
|
||||
})
|
||||
.forTypes(PackageType.MAC_PKG)
|
||||
.addBundleVerifier(SigningPackageTest::verifyPKG)
|
||||
|
||||
@@ -73,8 +73,8 @@ public class SigningPackageTwoStepTest {
|
||||
}
|
||||
|
||||
Path outputBundle = cmd.outputBundle();
|
||||
SigningBase.verifyPkgutil(outputBundle);
|
||||
SigningBase.verifySpctl(outputBundle, "install");
|
||||
SigningBase.verifyPkgutil(outputBundle, SigningBase.DEFAULT_INDEX);
|
||||
SigningBase.verifySpctl(outputBundle, "install", SigningBase.DEFAULT_INDEX);
|
||||
}
|
||||
|
||||
private static void verifyDMG(JPackageCommand cmd) {
|
||||
@@ -91,10 +91,10 @@ public class SigningPackageTwoStepTest {
|
||||
boolean isSigned = cmd.hasArgument("--mac-sign");
|
||||
Path launcherPath = ApplicationLayout.platformAppImage()
|
||||
.resolveAt(dmgImage).launchersDirectory().resolve(cmd.name());
|
||||
SigningBase.verifyCodesign(launcherPath, isSigned);
|
||||
SigningBase.verifyCodesign(dmgImage, isSigned);
|
||||
SigningBase.verifyCodesign(launcherPath, isSigned, SigningBase.DEFAULT_INDEX);
|
||||
SigningBase.verifyCodesign(dmgImage, isSigned, SigningBase.DEFAULT_INDEX);
|
||||
if (isSigned) {
|
||||
SigningBase.verifySpctl(dmgImage, "exec");
|
||||
SigningBase.verifySpctl(dmgImage, "exec", SigningBase.DEFAULT_INDEX);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -104,7 +104,7 @@ public class SigningPackageTwoStepTest {
|
||||
@Parameter("true")
|
||||
@Parameter("false")
|
||||
public static void test(boolean signAppImage) throws Exception {
|
||||
SigningCheck.checkCertificates();
|
||||
SigningCheck.checkCertificates(0);
|
||||
|
||||
Path appimageOutput = TKit.createTempDirectory("appimage");
|
||||
|
||||
@@ -113,9 +113,9 @@ public class SigningPackageTwoStepTest {
|
||||
if (signAppImage) {
|
||||
appImageCmd.addArguments("--mac-sign")
|
||||
.addArguments("--mac-signing-key-user-name",
|
||||
SigningBase.DEV_NAME)
|
||||
SigningBase.getDevName(0))
|
||||
.addArguments("--mac-signing-keychain",
|
||||
SigningBase.KEYCHAIN);
|
||||
SigningBase.getKeyChain());
|
||||
}
|
||||
|
||||
new PackageTest()
|
||||
@@ -127,9 +127,9 @@ public class SigningPackageTwoStepTest {
|
||||
if (signAppImage) {
|
||||
cmd.addArguments("--mac-sign",
|
||||
"--mac-signing-key-user-name",
|
||||
SigningBase.DEV_NAME,
|
||||
SigningBase.getDevName(0),
|
||||
"--mac-signing-keychain",
|
||||
SigningBase.KEYCHAIN);
|
||||
SigningBase.getKeyChain());
|
||||
}
|
||||
})
|
||||
.forTypes(PackageType.MAC_PKG)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
*/
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import jdk.jpackage.test.JPackageCommand;
|
||||
@@ -31,17 +32,53 @@ import jdk.jpackage.test.Executor.Result;
|
||||
|
||||
public class SigningBase {
|
||||
|
||||
public static String DEV_NAME;
|
||||
public static String APP_CERT;
|
||||
public static String INSTALLER_CERT;
|
||||
public static String KEYCHAIN;
|
||||
static {
|
||||
public static int DEFAULT_INDEX = 0;
|
||||
private static String [] DEV_NAMES = {
|
||||
"jpackage.openjdk.java.net",
|
||||
"jpackage.openjdk.java.net (ö)",
|
||||
};
|
||||
private static String DEFAULT_KEYCHAIN = "jpackagerTest.keychain";
|
||||
|
||||
public static String getDevName(int certIndex) {
|
||||
// Always use values from system properties if set
|
||||
String value = System.getProperty("jpackage.mac.signing.key.user.name");
|
||||
DEV_NAME = (value == null) ? "jpackage.openjdk.java.net" : value;
|
||||
APP_CERT = "Developer ID Application: " + DEV_NAME;
|
||||
INSTALLER_CERT = "Developer ID Installer: " + DEV_NAME;
|
||||
value = System.getProperty("jpackage.mac.signing.keychain");
|
||||
KEYCHAIN = (value == null) ? "jpackagerTest.keychain" : value;
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return DEV_NAMES[certIndex];
|
||||
}
|
||||
|
||||
public static int getDevNameIndex(String devName) {
|
||||
return Arrays.binarySearch(DEV_NAMES, devName);
|
||||
}
|
||||
|
||||
// Returns 'true' if dev name from DEV_NAMES
|
||||
public static boolean isDevNameDefault() {
|
||||
String value = System.getProperty("jpackage.mac.signing.key.user.name");
|
||||
if (value != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static String getAppCert(int certIndex) {
|
||||
return "Developer ID Application: " + getDevName(certIndex);
|
||||
}
|
||||
|
||||
public static String getInstallerCert(int certIndex) {
|
||||
return "Developer ID Installer: " + getDevName(certIndex);
|
||||
}
|
||||
|
||||
public static String getKeyChain() {
|
||||
// Always use values from system properties if set
|
||||
String value = System.getProperty("jpackage.mac.signing.keychain");
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return DEFAULT_KEYCHAIN;
|
||||
}
|
||||
|
||||
// Note: It is not clear if we can combine "--verify" and "--display", so
|
||||
@@ -63,13 +100,13 @@ public class SigningBase {
|
||||
int exitCode = 0;
|
||||
Executor executor = new Executor().setExecutable("/usr/bin/codesign");
|
||||
switch (type) {
|
||||
case CodesignCheckType.VERIFY_UNSIGNED:
|
||||
case VERIFY_UNSIGNED:
|
||||
exitCode = 1;
|
||||
case CodesignCheckType.VERIFY:
|
||||
case VERIFY:
|
||||
executor.addArguments("--verify", "--deep", "--strict",
|
||||
"--verbose=2", target.toString());
|
||||
break;
|
||||
case CodesignCheckType.DISPLAY:
|
||||
case DISPLAY:
|
||||
executor.addArguments("--display", "--verbose=4", target.toString());
|
||||
break;
|
||||
default:
|
||||
@@ -80,23 +117,23 @@ public class SigningBase {
|
||||
}
|
||||
|
||||
private static void verifyCodesignResult(List<String> result, Path target,
|
||||
boolean signed, CodesignCheckType type) {
|
||||
boolean signed, CodesignCheckType type, int certIndex) {
|
||||
result.stream().forEachOrdered(TKit::trace);
|
||||
String lookupString;
|
||||
switch (type) {
|
||||
case CodesignCheckType.VERIFY:
|
||||
case VERIFY:
|
||||
lookupString = target.toString() + ": valid on disk";
|
||||
checkString(result, lookupString);
|
||||
lookupString = target.toString() + ": satisfies its Designated Requirement";
|
||||
checkString(result, lookupString);
|
||||
break;
|
||||
case CodesignCheckType.VERIFY_UNSIGNED:
|
||||
case VERIFY_UNSIGNED:
|
||||
lookupString = target.toString() + ": code object is not signed at all";
|
||||
checkString(result, lookupString);
|
||||
break;
|
||||
case CodesignCheckType.DISPLAY:
|
||||
case DISPLAY:
|
||||
if (signed) {
|
||||
lookupString = "Authority=" + APP_CERT;
|
||||
lookupString = "Authority=" + getAppCert(certIndex);
|
||||
} else {
|
||||
lookupString = "Signature=adhoc";
|
||||
}
|
||||
@@ -124,7 +161,7 @@ public class SigningBase {
|
||||
}
|
||||
|
||||
private static void verifySpctlResult(List<String> output, Path target,
|
||||
String type, int exitCode) {
|
||||
String type, int exitCode, int certIndex) {
|
||||
output.stream().forEachOrdered(TKit::trace);
|
||||
String lookupString;
|
||||
|
||||
@@ -138,9 +175,9 @@ public class SigningBase {
|
||||
}
|
||||
|
||||
if (type.equals("install")) {
|
||||
lookupString = "origin=" + INSTALLER_CERT;
|
||||
lookupString = "origin=" + getInstallerCert(certIndex);
|
||||
} else {
|
||||
lookupString = "origin=" + APP_CERT;
|
||||
lookupString = "origin=" + getAppCert(certIndex);
|
||||
}
|
||||
checkString(output, lookupString);
|
||||
}
|
||||
@@ -155,20 +192,20 @@ public class SigningBase {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void verifyPkgutilResult(List<String> result) {
|
||||
private static void verifyPkgutilResult(List<String> result, int certIndex) {
|
||||
result.stream().forEachOrdered(TKit::trace);
|
||||
String lookupString = "Status: signed by";
|
||||
checkString(result, lookupString);
|
||||
lookupString = "1. " + INSTALLER_CERT;
|
||||
lookupString = "1. " + getInstallerCert(certIndex);
|
||||
checkString(result, lookupString);
|
||||
}
|
||||
|
||||
public static void verifyCodesign(Path target, boolean signed) {
|
||||
public static void verifyCodesign(Path target, boolean signed, int certIndex) {
|
||||
List<String> result = codesignResult(target, CodesignCheckType.VERIFY);
|
||||
verifyCodesignResult(result, target, signed, CodesignCheckType.VERIFY);
|
||||
verifyCodesignResult(result, target, signed, CodesignCheckType.VERIFY, certIndex);
|
||||
|
||||
result = codesignResult(target, CodesignCheckType.DISPLAY);
|
||||
verifyCodesignResult(result, target, signed, CodesignCheckType.DISPLAY);
|
||||
verifyCodesignResult(result, target, signed, CodesignCheckType.DISPLAY, certIndex);
|
||||
}
|
||||
|
||||
// Since we no longer have unsigned app image, but we need to check
|
||||
@@ -181,36 +218,36 @@ public class SigningBase {
|
||||
}
|
||||
|
||||
List<String> result = codesignResult(target, CodesignCheckType.VERIFY_UNSIGNED);
|
||||
verifyCodesignResult(result, target, false, CodesignCheckType.VERIFY_UNSIGNED);
|
||||
verifyCodesignResult(result, target, false, CodesignCheckType.VERIFY_UNSIGNED, -1);
|
||||
}
|
||||
|
||||
public static void verifySpctl(Path target, String type) {
|
||||
public static void verifySpctl(Path target, String type, int certIndex) {
|
||||
Result result = spctlResult(target, type);
|
||||
List<String> output = result.getOutput();
|
||||
|
||||
verifySpctlResult(output, target, type, result.getExitCode());
|
||||
verifySpctlResult(output, target, type, result.getExitCode(), certIndex);
|
||||
}
|
||||
|
||||
public static void verifyPkgutil(Path target) {
|
||||
public static void verifyPkgutil(Path target, int certIndex) {
|
||||
List<String> result = pkgutilResult(target);
|
||||
verifyPkgutilResult(result);
|
||||
verifyPkgutilResult(result, certIndex);
|
||||
}
|
||||
|
||||
public static void verifyAppImageSignature(JPackageCommand appImageCmd,
|
||||
boolean isSigned, String... launchers) throws Exception {
|
||||
Path launcherPath = appImageCmd.appLauncherPath();
|
||||
SigningBase.verifyCodesign(launcherPath, isSigned);
|
||||
SigningBase.verifyCodesign(launcherPath, isSigned, SigningBase.DEFAULT_INDEX);
|
||||
|
||||
final List<String> launchersList = List.of(launchers);
|
||||
launchersList.forEach(launcher -> {
|
||||
Path testALPath = launcherPath.getParent().resolve(launcher);
|
||||
SigningBase.verifyCodesign(testALPath, isSigned);
|
||||
SigningBase.verifyCodesign(testALPath, isSigned, SigningBase.DEFAULT_INDEX);
|
||||
});
|
||||
|
||||
Path appImage = appImageCmd.outputBundle();
|
||||
SigningBase.verifyCodesign(appImage, isSigned);
|
||||
SigningBase.verifyCodesign(appImage, isSigned, SigningBase.DEFAULT_INDEX);
|
||||
if (isSigned) {
|
||||
SigningBase.verifySpctl(appImage, "exec");
|
||||
SigningBase.verifySpctl(appImage, "exec", 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2019, 2023, 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
|
||||
@@ -33,41 +33,33 @@ import jdk.jpackage.internal.MacCertificate;
|
||||
|
||||
public class SigningCheck {
|
||||
|
||||
public static void checkCertificates() {
|
||||
List<String> result = findCertificate(SigningBase.APP_CERT, SigningBase.KEYCHAIN);
|
||||
String key = findKey(SigningBase.APP_CERT, result);
|
||||
validateCertificate(key);
|
||||
validateCertificateTrust(SigningBase.APP_CERT);
|
||||
|
||||
result = findCertificate(SigningBase.INSTALLER_CERT, SigningBase.KEYCHAIN);
|
||||
key = findKey(SigningBase.INSTALLER_CERT, result);
|
||||
validateCertificate(key);
|
||||
validateCertificateTrust(SigningBase.INSTALLER_CERT);
|
||||
}
|
||||
|
||||
private static List<String> findCertificate(String name, String keyChain) {
|
||||
List<String> result = new Executor()
|
||||
.setExecutable("/usr/bin/security")
|
||||
.addArguments("find-certificate", "-c", name, "-a", keyChain)
|
||||
.executeAndGetOutput();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String findKey(String name, List<String> result) {
|
||||
Pattern p = Pattern.compile("\"alis\"<blob>=\"([^\"]+)\"");
|
||||
Matcher m = p.matcher(result.stream().collect(Collectors.joining()));
|
||||
if (!m.find()) {
|
||||
TKit.trace("Did not found a key for '" + name + "'");
|
||||
return null;
|
||||
public static void checkCertificates(int certIndex) {
|
||||
if (!SigningBase.isDevNameDefault()) {
|
||||
// Do not validate user supplied certificates.
|
||||
// User supplied certs whose trust is set to "Use System Defaults"
|
||||
// will not be listed as trusted by dump-trust-settings, so we
|
||||
// cannot verify them completely.
|
||||
return;
|
||||
}
|
||||
String matchedKey = m.group(1);
|
||||
if (m.find()) {
|
||||
TKit.trace("Found more than one key for '" + name + "'");
|
||||
return null;
|
||||
|
||||
// Index can be -1 for unsigned tests, but we still skipping test
|
||||
// if machine is not configured for signing testing, so default it to
|
||||
// SigningBase.DEFAULT_INDEX
|
||||
if (certIndex <= -1) {
|
||||
certIndex = SigningBase.DEFAULT_INDEX;
|
||||
}
|
||||
TKit.trace("Using key '" + matchedKey);
|
||||
return matchedKey;
|
||||
|
||||
String key = MacCertificate.findCertificateKey(null,
|
||||
SigningBase.getAppCert(certIndex),
|
||||
SigningBase.getKeyChain());
|
||||
validateCertificate(key);
|
||||
validateCertificateTrust(SigningBase.getAppCert(certIndex));
|
||||
|
||||
key = MacCertificate.findCertificateKey(null,
|
||||
SigningBase.getInstallerCert(certIndex),
|
||||
SigningBase.getKeyChain());
|
||||
validateCertificate(key);
|
||||
validateCertificateTrust(SigningBase.getInstallerCert(certIndex));
|
||||
}
|
||||
|
||||
private static void validateCertificate(String key) {
|
||||
@@ -85,20 +77,16 @@ public class SigningCheck {
|
||||
|
||||
private static void validateCertificateTrust(String name) {
|
||||
// Certificates using the default user name must be trusted by user.
|
||||
// User supplied certs whose trust is set to "Use System Defaults"
|
||||
// will not be listed as trusted by dump-trust-settings
|
||||
if (SigningBase.DEV_NAME.equals("jpackage.openjdk.java.net")) {
|
||||
List<String> result = new Executor()
|
||||
.setExecutable("/usr/bin/security")
|
||||
.addArguments("dump-trust-settings")
|
||||
.executeWithoutExitCodeCheckAndGetOutput();
|
||||
result.stream().forEachOrdered(TKit::trace);
|
||||
TKit.assertTextStream(name)
|
||||
.predicate((line, what) -> line.trim().endsWith(what))
|
||||
.orElseThrow(() -> TKit.throwSkippedException(
|
||||
"Certifcate not trusted by current user: " + name))
|
||||
.apply(result.stream());
|
||||
}
|
||||
List<String> result = new Executor()
|
||||
.setExecutable("/usr/bin/security")
|
||||
.addArguments("dump-trust-settings")
|
||||
.executeWithoutExitCodeCheckAndGetOutput();
|
||||
result.stream().forEachOrdered(TKit::trace);
|
||||
TKit.assertTextStream(name)
|
||||
.predicate((line, what) -> line.trim().endsWith(what))
|
||||
.orElseThrow(() -> TKit.throwSkippedException(
|
||||
"Certifcate not trusted by current user: " + name))
|
||||
.apply(result.stream());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user