8308042: [macos] Developer ID Application Certificate not picked up by jpackage if it contains UNICODE characters

Reviewed-by: asemenyuk
This commit is contained in:
Alexander Matveev
2023-08-23 20:22:12 +00:00
parent 38a9edfb7e
commit 57a322da9b
11 changed files with 387 additions and 199 deletions

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View 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);
}

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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());
}
}