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

135
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\"<blob>=\"([^\"]+)\"")
val m = regex.matcher(certificates)
if (!m.find()) {
@ -97,4 +100,34 @@ internal class MacSigner(
)
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()

46
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)
}
}

76
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() {

Loading…
Cancel
Save