From 26a930e32b7702b17123ef534efc0bf416516deb Mon Sep 17 00:00:00 2001 From: Jacob Burroughs Date: 2023年3月13日 16:08:28 -0500 Subject: [PATCH 1/2] macOS codesign fixes Fixes both having no entitlements file and signing the wrong path in `codesign` fixes #306 --- .../fvarrui/javapackager/packagers/MacPackager.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/github/fvarrui/javapackager/packagers/MacPackager.java b/src/main/java/io/github/fvarrui/javapackager/packagers/MacPackager.java index 5173f4f1..46dc48aa 100644 --- a/src/main/java/io/github/fvarrui/javapackager/packagers/MacPackager.java +++ b/src/main/java/io/github/fvarrui/javapackager/packagers/MacPackager.java @@ -195,13 +195,13 @@ private File preparePrecompiledStartupStub() throws Exception { private void codesign(String developerId, File entitlements, File appFile) throws Exception { - prepareEntitlementFile(entitlements); + entitlements = prepareEntitlementFile(entitlements); manualDeepSign(appFile, developerId, entitlements); } - private void prepareEntitlementFile(File entitlements) throws Exception { + private File prepareEntitlementFile(File entitlements) throws Exception { // if entitlements.plist file not specified, use a default one if (entitlements == null) { Logger.warn("Entitlements file not specified. Using defaults!"); @@ -210,6 +210,7 @@ private void prepareEntitlementFile(File entitlements) throws Exception { } else if (!entitlements.exists()) { throw new Exception("Entitlements file doesn't exist: " + entitlements); } + return entitlements; } private void manualDeepSign(File appFolder, String developerCertificateName, File entitlements) throws IOException, CommandLineException { @@ -237,7 +238,6 @@ private void manualDeepSign(File appFolder, String developerCertificateName, Fil // finally, sign the top level directory codesign(entitlements, developerCertificateName, appFolder); - } private void codesign(File entitlements, String developerCertificateName, File file) throws IOException, CommandLineException { @@ -248,7 +248,7 @@ private void codesign(File entitlements, String developerCertificateName, File f arguments.add(entitlements); arguments.add("-s"); arguments.add(developerCertificateName); - arguments.add(appFolder); + arguments.add(file); CommandUtils.execute("codesign", arguments); } From 5832c7950d2d03e5d877c45297a38260d4e608ef Mon Sep 17 00:00:00 2001 From: Jacob Burroughs Date: 2023年3月13日 23:33:47 -0500 Subject: [PATCH 2/2] Support notarizing macOS applications Also rework codesigning to follow the same pattern as jpackage, which matches the official guidelines of only explicitly signing executable code (dylibs and executables), properly wrapping the jdk in a macOS bundle, and only attaching entitlements/hardened runtime to executables. This combination of changes ensures that signing works even without preserving filesystem extended attributes, as tends to happen when zipping application bundles, because Mach-O files have signatures embedded in the file contents whereas all other file types have their signatures stored in FS extended attributes. (Non-code files do get signed, but only through the manifest on the bundle itself so the file doesn't need modification.) This has been tested on a clean macOS 13.2.1 VM that has gatekeeper using its default settings. The VM was disconnected from the internet after downloading to validate the notarization and stapling. The user is prompted to confirm opening an application that came from the internet, as one is with any properly signed and notarized application that was downloaded from the internet. fixes #286 --- docs/macosx-specific-properties.md | 6 +- .../javapackager/gradle/LinuxTaskConfig.java | 44 ++++++ .../fvarrui/javapackager/model/MacConfig.java | 34 ++++- .../javapackager/packagers/MacPackager.java | 141 +++++++++++++++--- src/main/resources/mac/Info.plist.vtl | 2 +- src/main/resources/mac/RuntimeInfo.plist.vtl | 24 +++ 6 files changed, 222 insertions(+), 29 deletions(-) create mode 100644 src/main/java/io/github/fvarrui/javapackager/gradle/LinuxTaskConfig.java create mode 100644 src/main/resources/mac/RuntimeInfo.plist.vtl diff --git a/docs/macosx-specific-properties.md b/docs/macosx-specific-properties.md index 57a4c291..d104c2e4 100644 --- a/docs/macosx-specific-properties.md +++ b/docs/macosx-specific-properties.md @@ -16,6 +16,8 @@ path/to/entitlements.plist true|false true|false + true|false + xcrun_notarytool_profile_name path/to/png @@ -62,11 +64,13 @@ ## Signing properties | Property | Mandatory | Default value | Description | -| ------------------ | --------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +|--------------------| --------- |---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| | `developerId` | :x: | | Signing identity. | | `entitlements` | :x: | | Path to [entitlements](https://developer.apple.com/documentation/bundleresources/entitlements) file. | | `codesignApp` | :x: | `true` | If it is set to `false`, generated app will not be codesigned. | | `hardenedCodesign` | :x: | `true` | If it is set to `true`, enable [hardened runtime](https://developer.apple.com/documentation/security/hardened_runtime) if MacOS version>= 10.13.6. | +| `notarizeApp` | :x: | `false` | If it is set to `true`, generated app will be submitted to apple for notarization and the ticket will be stapled. | +| `keyChainProfile` | :x: | | Profile name originally provided to `xcrun notarytool store-credentials`. Must be set if `notarizeApp` is `true`. | `macStartup` | :x: | `SCRIPT` | App startup type, using a `SCRIPT` or a binary (compiled version of the script: `UNIVERSAL`, `X86_64` or `ARM64`). | ## DMG generation properties diff --git a/src/main/java/io/github/fvarrui/javapackager/gradle/LinuxTaskConfig.java b/src/main/java/io/github/fvarrui/javapackager/gradle/LinuxTaskConfig.java new file mode 100644 index 00000000..3c6c9679 --- /dev/null +++ b/src/main/java/io/github/fvarrui/javapackager/gradle/LinuxTaskConfig.java @@ -0,0 +1,44 @@ +package io.github.fvarrui.javapackager.gradle; + +import io.github.fvarrui.javapackager.model.LinuxConfig; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public abstract class LinuxTaskConfig { + @Input + @Optional + public abstract ListProperty getCategories(); + @Input + @Optional + public abstract Property isGenerateDeb(); + @Input + @Optional + public abstract Property isGenerateRpm(); + @Input + @Optional + public abstract Property isGenerateAppImage();; + @Input + @Optional + public abstract RegularFileProperty getPngFile(); + @Input + @Optional + public abstract Property isWrapJar(); + + public LinuxConfig buildConfig() { + LinuxConfig ret = new LinuxConfig(); + ret.setCategories(getCategories().getOrElse(new ArrayList())); + ret.setGenerateDeb(isGenerateDeb().getOrElse(true)); + ret.setGenerateRpm(isGenerateRpm().getOrElse(true)); + ret.setGenerateAppImage(isGenerateAppImage().getOrElse(true)); + ret.setPngFile(getPngFile().getAsFile().getOrNull()); + ret.setWrapJar(isWrapJar().getOrElse(true)); + return ret; + } +} diff --git a/src/main/java/io/github/fvarrui/javapackager/model/MacConfig.java b/src/main/java/io/github/fvarrui/javapackager/model/MacConfig.java index 0a3c9076..2f28aec5 100644 --- a/src/main/java/io/github/fvarrui/javapackager/model/MacConfig.java +++ b/src/main/java/io/github/fvarrui/javapackager/model/MacConfig.java @@ -36,7 +36,10 @@ public class MacConfig implements Serializable { private File provisionProfile; private File customLauncher; private File customInfoPlist; + private File customRuntimeInfoPlist; private boolean codesignApp = true; + private boolean notarizeApp = false; + private String keyChainProfile; private InfoPlist infoPlist = new InfoPlist(); private boolean hardenedCodesign = true; private MacStartup macStartup = MacStartup.SCRIPT; @@ -209,6 +212,14 @@ public void setCustomInfoPlist(File customInfoPlist) { this.customInfoPlist = customInfoPlist; } + public File getCustomRuntimeInfoPlist() { + return customRuntimeInfoPlist; + } + + public void setCustomRuntimeInfoPlist(File customRuntimeInfoPlist) { + this.customRuntimeInfoPlist = customRuntimeInfoPlist; + } + public File getProvisionProfile() { return provisionProfile; } @@ -233,6 +244,22 @@ public void setCodesignApp(boolean codesignApp) { this.codesignApp = codesignApp; } + public boolean isNotarizeApp() { + return notarizeApp; + } + + public void setNotarizeApp(boolean notarizeApp) { + this.notarizeApp = notarizeApp; + } + + public String getKeyChainProfile() { + return keyChainProfile; + } + + public void setKeyChainProfile(String keyChainProfile) { + this.keyChainProfile = keyChainProfile; + } + public InfoPlist getInfoPlist() { return infoPlist; } @@ -266,9 +293,10 @@ public String toString() { + ", volumeName=" + volumeName + ", generateDmg=" + generateDmg + ", generatePkg=" + generatePkg + ", relocateJar=" + relocateJar + ", appId=" + appId + ", developerId=" + developerId + ", entitlements=" + entitlements + ", provisionProfile=" + provisionProfile + ", customLauncher=" - + customLauncher + ", customInfoPlist=" + customInfoPlist + ", codesignApp=" + codesignApp - + ", infoPlist=" + infoPlist + ", hardenedCodesign=" + hardenedCodesign + ", macStartup=" + macStartup - + "]"; + + customLauncher + ", customInfoPlist=" + customInfoPlist + ", customRuntimeInfoPlist=" + + customRuntimeInfoPlist + ", codesignApp=" + codesignApp + ", notarizeApp=" + notarizeApp + + ", keyChainProfile=" + keyChainProfile + ", infoPlist=" + infoPlist + ", hardenedCodesign=" + + hardenedCodesign + ", macStartup=" + macStartup + "]"; } /** diff --git a/src/main/java/io/github/fvarrui/javapackager/packagers/MacPackager.java b/src/main/java/io/github/fvarrui/javapackager/packagers/MacPackager.java index 46dc48aa..3b805ec8 100644 --- a/src/main/java/io/github/fvarrui/javapackager/packagers/MacPackager.java +++ b/src/main/java/io/github/fvarrui/javapackager/packagers/MacPackager.java @@ -9,11 +9,19 @@ import java.io.File; import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; /** * Packager for MacOS @@ -25,6 +33,7 @@ public class MacPackager extends Packager { private File resourcesFolder; private File javaFolder; private File macOSFolder; + private File jreBundleFolder; public File getAppFile() { return appFile; @@ -73,7 +82,8 @@ protected void doCreateAppStructure() throws Exception { // sets common folders this.executableDestinationFolder = macOSFolder; this.jarFileDestinationFolder = javaFolder; - this.jreDestinationFolder = new File(contentsFolder, "PlugIns/" + jreDirectoryName + "/Contents/Home"); + this.jreBundleFolder = new File(contentsFolder, "PlugIns/" + jreDirectoryName + ".jre"); + this.jreDestinationFolder = new File(jreBundleFolder, "Contents/Home"); this.resourcesDestinationFolder = resourcesFolder; } @@ -83,6 +93,9 @@ protected void doCreateAppStructure() throws Exception { */ @Override public File doCreateApp() throws Exception { + if(bundleJre) { + processRuntimeInfoPlistFile(); + } // copies jarfile to Java folder FileUtils.copyFileToFolder(jarFile, javaFolder); @@ -97,6 +110,8 @@ public File doCreateApp() throws Exception { codesign(); + notarize(); + return appFile; } @@ -157,6 +172,21 @@ private void processInfoPlistFile() throws Exception { Logger.info("Info.plist file created in " + infoPlistFile.getAbsolutePath()); } + /** + * Creates and writes the Info.plist inside the JRE if no custom file is specified. + * @throws Exception if anything goes wrong + */ + private void processRuntimeInfoPlistFile() throws Exception { + File infoPlistFile = new File(jreBundleFolder, "Contents/Info.plist"); + if(macConfig.getCustomRuntimeInfoPlist() != null && macConfig.getCustomRuntimeInfoPlist().isFile() && macConfig.getCustomRuntimeInfoPlist().canRead()){ + FileUtils.copyFileToFile(macConfig.getCustomRuntimeInfoPlist(), infoPlistFile); + } else { + VelocityUtils.render("mac/RuntimeInfo.plist.vtl", infoPlistFile, this); + XMLUtils.prettify(infoPlistFile); + } + Logger.info("RuntimeInfo.plist file created in " + infoPlistFile.getAbsolutePath()); + } + private void codesign() throws Exception { if (!Platform.mac.isCurrentPlatform()) { Logger.warn("Generated app could not be signed due to current platform is " + Platform.getCurrentPlatform()); @@ -167,6 +197,18 @@ private void codesign() throws Exception { } } + private void notarize() throws Exception { + if (!Platform.mac.isCurrentPlatform()) { + Logger.warn("Generated app could not be notarized due to current platform is " + Platform.getCurrentPlatform()); + } else if (!getMacConfig().isCodesignApp()) { + Logger.warn("App codesigning disabled. Cannot notarize unsigned app"); + } else if (!getMacConfig().isNotarizeApp()) { + Logger.warn("App notarization disabled"); + } else { + notarize(this.macConfig.getKeyChainProfile(), this.appFile); + } + } + private void processProvisionProfileFile() throws Exception { if (macConfig.getProvisionProfile() != null && macConfig.getProvisionProfile().isFile() && macConfig.getProvisionProfile().canRead()) { // file name must be 'embedded.provisionprofile' @@ -197,7 +239,7 @@ private void codesign(String developerId, File entitlements, File appFile) throw entitlements = prepareEntitlementFile(entitlements); - manualDeepSign(appFile, developerId, entitlements); + signAppBundle(appFile, developerId, entitlements); } @@ -213,25 +255,29 @@ private File prepareEntitlementFile(File entitlements) throws Exception { return entitlements; } - private void manualDeepSign(File appFolder, String developerCertificateName, File entitlements) throws IOException, CommandLineException { - - // codesign each file in app - List findCommandArgs = new ArrayList(); - findCommandArgs.add(appFolder); - findCommandArgs.add("-depth"); // execute 'codesign' in 'reverse order', i.e., deepest files first - findCommandArgs.add("-type"); - findCommandArgs.add("f"); // filter for files only - findCommandArgs.add("-exec"); - findCommandArgs.add("codesign"); - findCommandArgs.add("-f"); - addHardenedCodesign(findCommandArgs); - findCommandArgs.add("-s"); - findCommandArgs.add(developerCertificateName); - findCommandArgs.add("--entitlements"); - findCommandArgs.add(entitlements); - findCommandArgs.add("{}"); - findCommandArgs.add("\\;"); - CommandUtils.execute("find", findCommandArgs); + private void signAppBundle(File appFolder, String developerCertificateName, File entitlements) throws IOException, CommandLineException { +// Sign all embedded executables and dynamic libraries +// Structure and order adapted from the JRE's jpackage + try (Stream stream = Files.walk(appFolder.toPath())) { + stream.filter(p -> Files.isRegularFile(p) + && (Files.isExecutable(p) || p.toString().endsWith(".dylib")) + && !(p.toString().contains("dylib.dSYM/Contents")) + && !(p.equals(this.executable.toPath())) + ).forEach(p -> { + if (Files.isSymbolicLink(p)) { + Logger.debug("Skipping signing symlink: " + p); + } else { + try { + codesign(Files.isExecutable(p) ? entitlements : null, developerCertificateName, p.toFile()); + } catch (IOException | CommandLineException e) { + throw new RuntimeException(e); + } + } + }); + } + + // sign the JRE itself after signing all its contents + codesign(developerCertificateName, jreBundleFolder); // make sure the executable is signed last codesign(entitlements, developerCertificateName, this.executable); @@ -239,13 +285,20 @@ private void manualDeepSign(File appFolder, String developerCertificateName, Fil // finally, sign the top level directory codesign(entitlements, developerCertificateName, appFolder); } + + private void codesign(String developerCertificateName, File file) throws IOException, CommandLineException { + codesign(null, developerCertificateName, file); + } private void codesign(File entitlements, String developerCertificateName, File file) throws IOException, CommandLineException { List arguments = new ArrayList(); arguments.add("-f"); - addHardenedCodesign(arguments); - arguments.add("--entitlements"); - arguments.add(entitlements); + if(entitlements != null) { + addHardenedCodesign(arguments); + arguments.add("--entitlements"); + arguments.add(entitlements); + } + arguments.add("--timestamp"); arguments.add("-s"); arguments.add(developerCertificateName); arguments.add(file); @@ -263,4 +316,44 @@ private void addHardenedCodesign(Collection args){ } } + private void notarize(String keyChainProfile, File appFile) throws IOException, CommandLineException { + Path zippedApp = null; + try { + zippedApp = zipApp(appFile); + List notarizeArgs = new ArrayList(); + notarizeArgs.add("notarytool"); + notarizeArgs.add("submit"); + notarizeArgs.add(zippedApp.toString()); + notarizeArgs.add("--wait"); + notarizeArgs.add("--keychain-profile"); + notarizeArgs.add(keyChainProfile); + CommandUtils.execute("xcrun", notarizeArgs); + } finally { + if(zippedApp != null) { + Files.deleteIfExists(zippedApp); + } + } + + List stapleArgs = new ArrayList(); + stapleArgs.add("stapler"); + stapleArgs.add("staple"); + stapleArgs.add(appFile); + CommandUtils.execute("xcrun", stapleArgs); + } + + private Path zipApp(File appFile) throws IOException { + Path zipPath = assetsFolder.toPath().resolve(appFile.getName() + "-notarization.zip"); + try(ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipPath))) { + Path sourcePath = appFile.toPath(); + Files.walkFileTree(sourcePath, new SimpleFileVisitor() { + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + zos.putNextEntry(new ZipEntry(sourcePath.getParent().relativize(file).toString())); + Files.copy(file, zos); + zos.closeEntry(); + return FileVisitResult.CONTINUE; + } + }); + } + return zipPath; + } } diff --git a/src/main/resources/mac/Info.plist.vtl b/src/main/resources/mac/Info.plist.vtl index 4475958e..31ca5905 100644 --- a/src/main/resources/mac/Info.plist.vtl +++ b/src/main/resources/mac/Info.plist.vtl @@ -128,7 +128,7 @@ #if ($info.bundleJre) JAVA_HOME - Contents/PlugIns/${info.jreDirectoryName}/Contents/Home + Contents/PlugIns/${info.jreDirectoryName}.jre/Contents/Home #end #if($info.envPath) PATH diff --git a/src/main/resources/mac/RuntimeInfo.plist.vtl b/src/main/resources/mac/RuntimeInfo.plist.vtl new file mode 100644 index 00000000..40cc09ba --- /dev/null +++ b/src/main/resources/mac/RuntimeInfo.plist.vtl @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + + CFBundleIdentifier + ${info.macConfig.appId}.runtime.java + CFBundleInfoDictionaryVersion + 7.0 + CFBundleName + Java Runtime Image + CFBundlePackageType + BNDL + CFBundleShortVersionString + ${info.version} + CFBundleSignature + ???? + CFBundleVersion + ${info.version} + + \ No newline at end of file

AltStyle によって変換されたページ (->オリジナル) /