From 57348cbde3721c3b8fc910e681580def1141c51c Mon Sep 17 00:00:00 2001 From: Alexey Tsvetkov <654232+AlexeyTsvetkov@users.noreply.github.com> Date: Tue, 10 Jan 2023 21:08:28 +0100 Subject: [PATCH] Fix running unsigned apps locally on macOS Ventura (#2617) Normally macOS Gatekeeper does not allow unsigned apps to run. However, apps created on the same machine were allowed to run. This allows developers to test package apps on their machines without configuring Apple Developer ID. Previously, the Compose Multiplatform Gradle plugin simply did not do anything, when the signing was not configured. However, in macOS Ventura the Gatekeeper checks became stricker, so "unsigned" packaged apps started to be shown as "damaged". This seems to happen, because parts of a final app image were signed. Also they were signed by different certificates (a runtime image could be signed by a runtime vendor, while Skiko binary is signed by JetBrains). This change removes all signatures if signing is not configured. See also https://bugs.openjdk.org/browse/JDK-8276150 Fixes #2476 --- .../desktop/application/internal/MacSigner.kt | 135 +++++++++++------- .../application/internal/MacSigningHelper.kt | 46 +----- .../application/tasks/AbstractJPackageTask.kt | 76 ++++++---- 3 files changed, 134 insertions(+), 123 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt index a38cab986d..eb642f5a2a 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt @@ -6,19 +6,54 @@ package org.jetbrains.compose.desktop.application.internal import org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSSigningSettings +import org.jetbrains.compose.internal.utils.Arch import org.jetbrains.compose.internal.utils.MacUtils +import org.jetbrains.compose.internal.utils.currentArch import java.io.File -import java.nio.file.Files import java.util.regex.Pattern +import kotlin.io.path.isExecutable -internal class MacSigner( - val settings: ValidatedMacOSSigningSettings, - private val runExternalTool: ExternalToolRunner -) { +internal abstract class MacSigner(protected val runTool: ExternalToolRunner) { + /** + * If [entitlements] file is provided, executables are signed with entitlements. + * Set [forceEntitlements] to `true` to sign all types of files with the provided [entitlements]. + */ + abstract fun sign( + file: File, + entitlements: File? = null, + forceEntitlements: Boolean = false + ) + + fun unsign(file: File) { + runTool.unsign(file) + } + + abstract val settings: ValidatedMacOSSigningSettings? +} + +internal class NoCertificateSigner(runTool: ExternalToolRunner) : MacSigner(runTool) { + override fun sign(file: File, entitlements: File?, forceEntitlements: Boolean) { + unsign(file) + if (currentArch == Arch.Arm64) { + // Apple Silicon requires binaries to be signed + // For local builds, ad hoc signatures are OK + // https://wiki.lazarus.freepascal.org/Code_Signing_for_macOS + runTool.codesign("--sign", "-", "-vvvv", file.absolutePath) + } + } + + override val settings: ValidatedMacOSSigningSettings? + get() = null +} + +internal class MacSignerImpl( + override val settings: ValidatedMacOSSigningSettings, + runTool: ExternalToolRunner +) : MacSigner(runTool) { private lateinit var signKey: String init { - runExternalTool( + runTool( MacUtils.security, args = listOfNotNull( "find-certificate", @@ -27,58 +62,26 @@ internal class MacSigner( settings.fullDeveloperID, settings.keychain?.absolutePath ), - processStdout = { stdout -> - signKey = findCertificate(stdout) - } + processStdout = { signKey = matchCertificates(it) } ) } - /** - * If [entitlements] file is provided, executables are signed with entitlements. - * Set [forceEntitlements] to `true` to sign all types of files with the provided [entitlements]. - */ - fun sign( + override fun sign( file: File, - entitlements: File? = null, - forceEntitlements: Boolean = false + entitlements: File?, + forceEntitlements: Boolean ) { - val args = arrayListOf( - "-vvvv", - "--timestamp", - "--options", "runtime", - "--force", - "--prefix", settings.prefix, - "--sign", signKey + runTool.unsign(file) + runTool.sign( + file = file, + signKey = signKey, + entitlements = entitlements?.takeIf { forceEntitlements || file.isExecutable }, + prefix = settings.prefix, + keychain = settings.keychain ) - - settings.keychain?.let { - args.add("--keychain") - args.add(it.absolutePath) - } - - if (forceEntitlements || Files.isExecutable(file.toPath())) { - entitlements?.let { - args.add("--entitlements") - args.add(it.absolutePath) - } - } - - args.add(file.absolutePath) - - runExternalTool(MacUtils.codesign, args) } - fun unsign(file: File) { - val args = listOf( - "-vvvv", - "--remove-signature", - file.absolutePath - ) - runExternalTool(MacUtils.codesign, args) - - } - - private fun findCertificate(certificates: String): String { + private fun matchCertificates(certificates: String): String { val regex = Pattern.compile("\"alis\"=\"([^\"]+)\"") val m = regex.matcher(certificates) if (!m.find()) { @@ -97,4 +100,34 @@ internal class MacSigner( ) return result } -} \ No newline at end of file +} + +private fun ExternalToolRunner.codesign(vararg args: String) = + this(MacUtils.codesign, args.toList()) + +private fun ExternalToolRunner.unsign(file: File) = + codesign("-vvvv", "--remove-signature", file.absolutePath) + +private fun ExternalToolRunner.sign( + file: File, + signKey: String, + entitlements: File?, + prefix: String?, + keychain: File? +) = codesign( + "-vvvv", + "--timestamp", + "--options", "runtime", + "--force", + *optionalArg("--prefix", prefix), + "--sign", signKey, + *optionalArg("--keychain", keychain?.absolutePath), + *optionalArg("--entitlements", entitlements?.absolutePath), + file.absolutePath +) + +private fun optionalArg(arg: String, value: String?): Array = + if (value != null) arrayOf(arg, value) else emptyArray() + +private val File.isExecutable: Boolean + get() = toPath().isExecutable() \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigningHelper.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigningHelper.kt index 0ece83a9a7..6ce422410c 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigningHelper.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigningHelper.kt @@ -7,9 +7,9 @@ package org.jetbrains.compose.desktop.application.internal import org.jetbrains.compose.desktop.application.internal.files.isDylibPath import java.io.File +import java.nio.file.* import kotlin.io.path.isExecutable import kotlin.io.path.isRegularFile -import kotlin.io.path.isSymbolicLink internal class MacSigningHelper( private val macSigner: MacSigner, @@ -23,58 +23,18 @@ internal class MacSigningHelper( private val runtimeDir = appDir.resolve("Contents/runtime") fun modifyRuntimeIfNeeded() { - // Only resign modify the runtime if a provisioning profile or alternative entitlements file is provided. - // If no entitlements file is provided, the runtime cannot be resigned. - if (runtimeProvisioningProfile == null && - // When resigning the runtime, an app entitlements file is also needed. - (runtimeEntitlementsFile == null || entitlementsFile == null) - ) { - return - } - // Add the provisioning profile - runtimeProvisioningProfile?.let { - addRuntimeProvisioningProfile(runtimeDir, it) - } // Resign the runtime completely (and also the app dir only) - resignRuntimeAndAppDir(appDir, runtimeDir) - } - - private fun addRuntimeProvisioningProfile( - runtimeDir: File, - runtimeProvisioningProfile: File - ) { - runtimeProvisioningProfile.copyTo( - target = runtimeDir.resolve("Contents/embedded.provisionprofile"), - overwrite = true - ) - } - - private fun resignRuntimeAndAppDir( - appDir: File, - runtimeDir: File - ) { // Sign all libs and executables in runtime runtimeDir.walk().forEach { file -> val path = file.toPath() - if (path.isRegularFile() && (path.isExecutable() || path.toString().isDylibPath)) { - if (path.isSymbolicLink()) { - // Ignore symbolic links - } else { - // Resign file - macSigner.unsign(file) - macSigner.sign(file, runtimeEntitlementsFile) - } + if (path.isRegularFile(LinkOption.NOFOLLOW_LINKS) && (path.isExecutable() || file.name.isDylibPath)) { + macSigner.sign(file, runtimeEntitlementsFile) } } - // Resign runtime directory - macSigner.unsign(runtimeDir) macSigner.sign(runtimeDir, runtimeEntitlementsFile, forceEntitlements = true) - - // Resign app directory (contents other than runtime were already signed by jpackage) - macSigner.unsign(appDir) macSigner.sign(appDir, entitlementsFile, forceEntitlements = true) } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt index fca6a964a4..6653d9e1f0 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt @@ -23,11 +23,14 @@ import org.jetbrains.compose.desktop.application.internal.JvmRuntimeProperties import org.jetbrains.compose.desktop.application.internal.validation.validate import org.jetbrains.compose.internal.utils.* import java.io.* +import java.nio.file.LinkOption import java.util.* import javax.inject.Inject import kotlin.collections.HashMap import kotlin.collections.HashSet import kotlin.collections.ArrayList +import kotlin.io.path.isExecutable +import kotlin.io.path.isRegularFile abstract class AbstractJPackageTask @Inject constructor( @get:Input @@ -237,12 +240,17 @@ abstract class AbstractJPackageTask @Inject constructor( @get:Nested internal var nonValidatedMacSigningSettings: MacOSSigningSettings? = null + private val shouldSign: Boolean + get() = nonValidatedMacSigningSettings?.sign?.get() == true + private val macSigner: MacSigner? by lazy { val nonValidatedSettings = nonValidatedMacSigningSettings - if (currentOS == OS.MacOS && nonValidatedSettings?.sign?.get() == true) { - val validatedSettings = - nonValidatedSettings.validate(nonValidatedMacBundleID, project, macAppStore) - MacSigner(validatedSettings, runExternalTool) + if (currentOS == OS.MacOS) { + if (shouldSign) { + val validatedSettings = + nonValidatedSettings!!.validate(nonValidatedMacBundleID, project, macAppStore) + MacSignerImpl(validatedSettings, runExternalTool) + } else NoCertificateSigner(runExternalTool) } else null } @@ -389,11 +397,11 @@ abstract class AbstractJPackageTask @Inject constructor( cliArg("--mac-app-category", macAppCategory) cliArg("--mac-entitlements", macEntitlementsFile) - macSigner?.let { signer -> + macSigner?.settings?.let { signingSettings -> cliArg("--mac-sign", true) - cliArg("--mac-signing-key-user-name", signer.settings.identity) - cliArg("--mac-signing-keychain", signer.settings.keychain) - cliArg("--mac-package-signing-prefix", signer.settings.prefix) + cliArg("--mac-signing-key-user-name", signingSettings.identity) + cliArg("--mac-signing-keychain", signingSettings.keychain) + cliArg("--mac-package-signing-prefix", signingSettings.prefix) } } } @@ -434,19 +442,16 @@ abstract class AbstractJPackageTask @Inject constructor( return outdatedLibs } + private fun jarCopyingProcessor(): FileCopyingProcessor = + if (currentOS == OS.MacOS) { + val tmpDirForSign = signDir.ioFile + fileOperations.clearDirs(tmpDirForSign) + MacJarSignFileCopyingProcessor(macSigner!!, tmpDirForSign, jvmRuntimeInfo.majorVersion) + } else SimpleFileCopyingProcessor + override fun prepareWorkingDir(inputChanges: InputChanges) { val libsDir = libsDir.ioFile - val fileProcessor = - macSigner?.let { signer -> - val tmpDirForSign = signDir.ioFile - fileOperations.clearDirs(tmpDirForSign) - - MacJarSignFileCopyingProcessor( - signer, - tmpDirForSign, - jvmRuntimeVersion = jvmRuntimeInfo.majorVersion - ) - } ?: SimpleFileCopyingProcessor + val fileProcessor = jarCopyingProcessor() val mangleJarFilesNames = mangleJarFilesNames.get() fun copyFileToLibsDir(sourceFile: File): File { @@ -525,17 +530,30 @@ abstract class AbstractJPackageTask @Inject constructor( private fun modifyRuntimeOnMacOsIfNeeded() { if (currentOS != OS.MacOS || targetFormat != TargetFormat.AppImage) return - macSigner?.let { macSigner -> - val macSigningHelper = MacSigningHelper( - macSigner = macSigner, - runtimeProvisioningProfile = macRuntimeProvisioningProfile.ioFileOrNull, - entitlementsFile = macEntitlementsFile.ioFileOrNull, - runtimeEntitlementsFile = macRuntimeEntitlementsFile.ioFileOrNull, - destinationDir = destinationDir.ioFile, - packageName = packageName.get() - ) - macSigningHelper.modifyRuntimeIfNeeded() + + val appDir = destinationDir.ioFile.resolve("${packageName.get()}.app") + val runtimeDir = appDir.resolve("Contents/runtime") + + // Add the provisioning profile + macRuntimeProvisioningProfile.ioFileOrNull?.copyTo( + target = runtimeDir.resolve("Contents/embedded.provisionprofile"), + overwrite = true + ) + val appEntitlementsFile = macEntitlementsFile.ioFileOrNull + val runtimeEntitlementsFile = macRuntimeEntitlementsFile.ioFileOrNull + + val macSigner = macSigner!! + // Resign the runtime completely (and also the app dir only) + // Sign all libs and executables in runtime + runtimeDir.walk().forEach { file -> + val path = file.toPath() + if (path.isRegularFile(LinkOption.NOFOLLOW_LINKS) && (path.isExecutable() || file.name.isDylibPath)) { + macSigner.sign(file, runtimeEntitlementsFile) + } } + + macSigner.sign(runtimeDir, runtimeEntitlementsFile, forceEntitlements = true) + macSigner.sign(appDir, appEntitlementsFile, forceEntitlements = true) } override fun initState() {