Browse Source

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
pull/2623/head
Alexey Tsvetkov 2 years ago committed by GitHub
parent
commit
57348cbde3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 131
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt
  2. 44
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigningHelper.kt
  3. 70
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt

131
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt

@ -6,79 +6,82 @@
package org.jetbrains.compose.desktop.application.internal package org.jetbrains.compose.desktop.application.internal
import org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSSigningSettings 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.MacUtils
import org.jetbrains.compose.internal.utils.currentArch
import java.io.File import java.io.File
import java.nio.file.Files
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.io.path.isExecutable
internal class MacSigner( internal abstract class MacSigner(protected val runTool: ExternalToolRunner) {
val settings: ValidatedMacOSSigningSettings,
private val runExternalTool: ExternalToolRunner
) {
private lateinit var signKey: String
init {
runExternalTool(
MacUtils.security,
args = listOfNotNull(
"find-certificate",
"-a",
"-c",
settings.fullDeveloperID,
settings.keychain?.absolutePath
),
processStdout = { stdout ->
signKey = findCertificate(stdout)
}
)
}
/** /**
* If [entitlements] file is provided, executables are signed with entitlements. * If [entitlements] file is provided, executables are signed with entitlements.
* Set [forceEntitlements] to `true` to sign all types of files with the provided [entitlements]. * Set [forceEntitlements] to `true` to sign all types of files with the provided [entitlements].
*/ */
fun sign( abstract fun sign(
file: File, file: File,
entitlements: File? = null, entitlements: File? = null,
forceEntitlements: Boolean = false forceEntitlements: Boolean = false
) {
val args = arrayListOf(
"-vvvv",
"--timestamp",
"--options", "runtime",
"--force",
"--prefix", settings.prefix,
"--sign", signKey
) )
settings.keychain?.let { fun unsign(file: File) {
args.add("--keychain") runTool.unsign(file)
args.add(it.absolutePath)
} }
if (forceEntitlements || Files.isExecutable(file.toPath())) { abstract val settings: ValidatedMacOSSigningSettings?
entitlements?.let { }
args.add("--entitlements")
args.add(it.absolutePath) 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)
} }
} }
args.add(file.absolutePath) override val settings: ValidatedMacOSSigningSettings?
get() = null
}
runExternalTool(MacUtils.codesign, args) internal class MacSignerImpl(
} override val settings: ValidatedMacOSSigningSettings,
runTool: ExternalToolRunner
) : MacSigner(runTool) {
private lateinit var signKey: String
fun unsign(file: File) { init {
val args = listOf( runTool(
"-vvvv", MacUtils.security,
"--remove-signature", args = listOfNotNull(
file.absolutePath "find-certificate",
"-a",
"-c",
settings.fullDeveloperID,
settings.keychain?.absolutePath
),
processStdout = { signKey = matchCertificates(it) }
) )
runExternalTool(MacUtils.codesign, args) }
override fun sign(
file: File,
entitlements: File?,
forceEntitlements: Boolean
) {
runTool.unsign(file)
runTool.sign(
file = file,
signKey = signKey,
entitlements = entitlements?.takeIf { forceEntitlements || file.isExecutable },
prefix = settings.prefix,
keychain = settings.keychain
)
} }
private fun findCertificate(certificates: String): String { private fun matchCertificates(certificates: String): String {
val regex = Pattern.compile("\"alis\"<blob>=\"([^\"]+)\"") val regex = Pattern.compile("\"alis\"<blob>=\"([^\"]+)\"")
val m = regex.matcher(certificates) val m = regex.matcher(certificates)
if (!m.find()) { if (!m.find()) {
@ -98,3 +101,33 @@ internal class MacSigner(
return result return result
} }
} }
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<String> =
if (value != null) arrayOf(arg, value) else emptyArray()
private val File.isExecutable: Boolean
get() = toPath().isExecutable()

44
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 org.jetbrains.compose.desktop.application.internal.files.isDylibPath
import java.io.File import java.io.File
import java.nio.file.*
import kotlin.io.path.isExecutable import kotlin.io.path.isExecutable
import kotlin.io.path.isRegularFile import kotlin.io.path.isRegularFile
import kotlin.io.path.isSymbolicLink
internal class MacSigningHelper( internal class MacSigningHelper(
private val macSigner: MacSigner, private val macSigner: MacSigner,
@ -23,58 +23,18 @@ internal class MacSigningHelper(
private val runtimeDir = appDir.resolve("Contents/runtime") private val runtimeDir = appDir.resolve("Contents/runtime")
fun modifyRuntimeIfNeeded() { 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) // 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 // Sign all libs and executables in runtime
runtimeDir.walk().forEach { file -> runtimeDir.walk().forEach { file ->
val path = file.toPath() val path = file.toPath()
if (path.isRegularFile() && (path.isExecutable() || path.toString().isDylibPath)) { if (path.isRegularFile(LinkOption.NOFOLLOW_LINKS) && (path.isExecutable() || file.name.isDylibPath)) {
if (path.isSymbolicLink()) {
// Ignore symbolic links
} else {
// Resign file
macSigner.unsign(file)
macSigner.sign(file, runtimeEntitlementsFile) macSigner.sign(file, runtimeEntitlementsFile)
} }
} }
}
// Resign runtime directory
macSigner.unsign(runtimeDir)
macSigner.sign(runtimeDir, runtimeEntitlementsFile, forceEntitlements = true) 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) macSigner.sign(appDir, entitlementsFile, forceEntitlements = true)
} }
} }

70
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.desktop.application.internal.validation.validate
import org.jetbrains.compose.internal.utils.* import org.jetbrains.compose.internal.utils.*
import java.io.* import java.io.*
import java.nio.file.LinkOption
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlin.collections.HashSet import kotlin.collections.HashSet
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.io.path.isExecutable
import kotlin.io.path.isRegularFile
abstract class AbstractJPackageTask @Inject constructor( abstract class AbstractJPackageTask @Inject constructor(
@get:Input @get:Input
@ -237,12 +240,17 @@ abstract class AbstractJPackageTask @Inject constructor(
@get:Nested @get:Nested
internal var nonValidatedMacSigningSettings: MacOSSigningSettings? = null internal var nonValidatedMacSigningSettings: MacOSSigningSettings? = null
private val shouldSign: Boolean
get() = nonValidatedMacSigningSettings?.sign?.get() == true
private val macSigner: MacSigner? by lazy { private val macSigner: MacSigner? by lazy {
val nonValidatedSettings = nonValidatedMacSigningSettings val nonValidatedSettings = nonValidatedMacSigningSettings
if (currentOS == OS.MacOS && nonValidatedSettings?.sign?.get() == true) { if (currentOS == OS.MacOS) {
if (shouldSign) {
val validatedSettings = val validatedSettings =
nonValidatedSettings.validate(nonValidatedMacBundleID, project, macAppStore) nonValidatedSettings!!.validate(nonValidatedMacBundleID, project, macAppStore)
MacSigner(validatedSettings, runExternalTool) MacSignerImpl(validatedSettings, runExternalTool)
} else NoCertificateSigner(runExternalTool)
} else null } else null
} }
@ -389,11 +397,11 @@ abstract class AbstractJPackageTask @Inject constructor(
cliArg("--mac-app-category", macAppCategory) cliArg("--mac-app-category", macAppCategory)
cliArg("--mac-entitlements", macEntitlementsFile) cliArg("--mac-entitlements", macEntitlementsFile)
macSigner?.let { signer -> macSigner?.settings?.let { signingSettings ->
cliArg("--mac-sign", true) cliArg("--mac-sign", true)
cliArg("--mac-signing-key-user-name", signer.settings.identity) cliArg("--mac-signing-key-user-name", signingSettings.identity)
cliArg("--mac-signing-keychain", signer.settings.keychain) cliArg("--mac-signing-keychain", signingSettings.keychain)
cliArg("--mac-package-signing-prefix", signer.settings.prefix) cliArg("--mac-package-signing-prefix", signingSettings.prefix)
} }
} }
} }
@ -434,19 +442,16 @@ abstract class AbstractJPackageTask @Inject constructor(
return outdatedLibs return outdatedLibs
} }
override fun prepareWorkingDir(inputChanges: InputChanges) { private fun jarCopyingProcessor(): FileCopyingProcessor =
val libsDir = libsDir.ioFile if (currentOS == OS.MacOS) {
val fileProcessor =
macSigner?.let { signer ->
val tmpDirForSign = signDir.ioFile val tmpDirForSign = signDir.ioFile
fileOperations.clearDirs(tmpDirForSign) fileOperations.clearDirs(tmpDirForSign)
MacJarSignFileCopyingProcessor(macSigner!!, tmpDirForSign, jvmRuntimeInfo.majorVersion)
} else SimpleFileCopyingProcessor
MacJarSignFileCopyingProcessor( override fun prepareWorkingDir(inputChanges: InputChanges) {
signer, val libsDir = libsDir.ioFile
tmpDirForSign, val fileProcessor = jarCopyingProcessor()
jvmRuntimeVersion = jvmRuntimeInfo.majorVersion
)
} ?: SimpleFileCopyingProcessor
val mangleJarFilesNames = mangleJarFilesNames.get() val mangleJarFilesNames = mangleJarFilesNames.get()
fun copyFileToLibsDir(sourceFile: File): File { fun copyFileToLibsDir(sourceFile: File): File {
@ -525,17 +530,30 @@ abstract class AbstractJPackageTask @Inject constructor(
private fun modifyRuntimeOnMacOsIfNeeded() { private fun modifyRuntimeOnMacOsIfNeeded() {
if (currentOS != OS.MacOS || targetFormat != TargetFormat.AppImage) return if (currentOS != OS.MacOS || targetFormat != TargetFormat.AppImage) return
macSigner?.let { macSigner ->
val macSigningHelper = MacSigningHelper( val appDir = destinationDir.ioFile.resolve("${packageName.get()}.app")
macSigner = macSigner, val runtimeDir = appDir.resolve("Contents/runtime")
runtimeProvisioningProfile = macRuntimeProvisioningProfile.ioFileOrNull,
entitlementsFile = macEntitlementsFile.ioFileOrNull, // Add the provisioning profile
runtimeEntitlementsFile = macRuntimeEntitlementsFile.ioFileOrNull, macRuntimeProvisioningProfile.ioFileOrNull?.copyTo(
destinationDir = destinationDir.ioFile, target = runtimeDir.resolve("Contents/embedded.provisionprofile"),
packageName = packageName.get() overwrite = true
) )
macSigningHelper.modifyRuntimeIfNeeded() 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() { override fun initState() {

Loading…
Cancel
Save