Skip to content

Commit f68722a

Browse files
committed
fix(macOS): Enabled hardened runtime in code signing.
Signed-off-by: Iva Horn <iva.horn@nextcloud.com>
1 parent 0d4532e commit f68722a

3 files changed

Lines changed: 116 additions & 8 deletions

File tree

admin/osx/mac-crafter/Sources/Utils/Signer.swift

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,104 @@ enum Signer: Signing {
9595

9696
return items
9797
}
98-
98+
99+
///
100+
/// Check whether the given file is an native executable binary or not.
101+
///
102+
private static func isExecutable(_ file: URL) async throws -> Bool {
103+
let outPipe = Pipe()
104+
let errPipe = Pipe()
105+
let task = Process()
106+
task.standardOutput = outPipe
107+
task.standardError = errPipe
108+
109+
let command = "file \"\(file.path)\""
110+
111+
guard await run("/bin/zsh", ["-c", command], task: task) == 0 else {
112+
throw MacCrafterError.signing("Failed to determine if \(file.path) is an executable.")
113+
}
114+
115+
let outputFileHandle = outPipe.fileHandleForReading
116+
let outputData = outputFileHandle.readDataToEndOfFile()
117+
try outputFileHandle.close()
118+
let output = String(data: outputData, encoding: .utf8) ?? ""
119+
120+
return output.contains("Mach-O 64-bit executable")
121+
}
122+
123+
///
124+
/// Find and sign the Qt web engine helper app inside the QtWebEngineCore framework.
125+
///
126+
/// This needs explicit treatment because codesign does not automatically sign it when signing the upstream framework bundle.
127+
///
128+
private static func signQtWebEngineProcessApp(in bundle: URL, with codeSignIdentity: String) async {
129+
let location = bundle
130+
.appendingPathComponent("Contents")
131+
.appendingPathComponent("Frameworks")
132+
.appendingPathComponent("QtWebEngineCore.framework")
133+
.appendingPathComponent("Versions")
134+
.appendingPathComponent("A")
135+
.appendingPathComponent("Helpers")
136+
.appendingPathComponent("QtWebEngineProcess.app")
137+
138+
await sign(at: location, with: codeSignIdentity, entitlements: nil)
139+
}
140+
141+
///
142+
/// Find and sign the Sparkle downloader inside the Sparkle framework.
143+
///
144+
/// This needs explicit treatment because codesign does not automatically sign it when signing the upstream framework bundle.
145+
///
146+
private static func signSparkleDownloader(in bundle: URL, with codeSignIdentity: String) async {
147+
let location = bundle
148+
.appendingPathComponent("Contents")
149+
.appendingPathComponent("Frameworks")
150+
.appendingPathComponent("Sparkle.framework")
151+
.appendingPathComponent("Versions")
152+
.appendingPathComponent("B")
153+
.appendingPathComponent("XPCServices")
154+
.appendingPathComponent("Downloader.xpc")
155+
156+
await sign(at: location, with: codeSignIdentity, entitlements: nil)
157+
}
158+
159+
///
160+
/// Find and sign the Sparkle updater app inside the Sparkle framework.
161+
///
162+
/// This needs explicit treatment because codesign does not automatically sign it when signing the upstream framework bundle.
163+
///
164+
private static func signSparkleUpdaterApp(in bundle: URL, with codeSignIdentity: String) async {
165+
let location = bundle
166+
.appendingPathComponent("Contents")
167+
.appendingPathComponent("Frameworks")
168+
.appendingPathComponent("Sparkle.framework")
169+
.appendingPathComponent("Versions")
170+
.appendingPathComponent("B")
171+
.appendingPathComponent("Updater.app")
172+
173+
await sign(at: location, with: codeSignIdentity, entitlements: nil)
174+
}
175+
176+
///
177+
/// There may be additional executables in the binaries directory which also need to be signed.
178+
///
179+
private static func signAdditionalBinaries(in bundle: URL, with codeSignIdentity: String) async throws {
180+
let location = bundle
181+
.appendingPathComponent("Contents")
182+
.appendingPathComponent("MacOS")
183+
184+
let candidates = try FileManager.default.contentsOfDirectory(at: location, includingPropertiesForKeys: nil)
185+
186+
for candidate in candidates {
187+
if try await isExecutable(candidate) {
188+
await sign(at: candidate, with: codeSignIdentity, entitlements: nil)
189+
}
190+
}
191+
192+
await sign(at: location.appendingPathComponent("nextcloudcmd"), with: codeSignIdentity, entitlements: nil)
193+
await sign(at: location.appendingPathComponent("nextclouddevcmd"), with: codeSignIdentity, entitlements: nil)
194+
}
195+
99196
private static func verify(at location: URL) async throws {
100197
Log.info("Verifying: \(location.path)")
101198
let code = await shell("codesign --verify --deep --strict --verbose=2 \"\(location.path)\"")
@@ -132,7 +229,11 @@ enum Signer: Signing {
132229

133230
await sign(at: extensionInMainBundle, with: codeSignIdentity, entitlements: extensionEntitlements)
134231
}
135-
232+
233+
await signQtWebEngineProcessApp(in: location, with: codeSignIdentity)
234+
await signSparkleDownloader(in: location, with: codeSignIdentity)
235+
await signSparkleUpdaterApp(in: location, with: codeSignIdentity)
236+
136237
let frameworksInsideMainBundle = try findFrameworks(at: location)
137238

138239
try await withThrowingTaskGroup(of: Void.self) { group in
@@ -163,8 +264,7 @@ enum Signer: Signing {
163264
await sign(at: dynamicLibrary, with: codeSignIdentity, entitlements: nil)
164265
}
165266

166-
await sign(at: binariesLocation.appendingPathComponent("nextcloudcmd"), with: codeSignIdentity, entitlements: nil)
167-
await sign(at: binariesLocation.appendingPathComponent("nextclouddevcmd"), with: codeSignIdentity, entitlements: nil)
267+
try await signAdditionalBinaries(in: location, with: codeSignIdentity)
168268

169269
guard let mainAppEntitlements = entitlements[location.lastPathComponent] else {
170270
throw MacCrafterError.signing("No entitlements provided for: \(location.path)")
@@ -188,10 +288,11 @@ enum Signer: Signing {
188288
"codesign",
189289
location.path,
190290
"--timestamp",
191-
"--verbose=4",
192-
"--preserve-metadata=entitlements",
291+
"--verbose 4",
292+
"--preserve-metadata entitlements",
193293
"--force",
194-
"--sign=\"\(codeSignIdentity)\""
294+
"--options runtime",
295+
"--sign \"\(codeSignIdentity)\""
195296
]
196297

197298
if let entitlements {

shell_integration/MacOSX/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ if(APPLE)
2828
add_custom_target( mac_fileproviderplugin ALL
2929
xcodebuild ARCHS=${CMAKE_OSX_ARCHITECTURES} ONLY_ACTIVE_ARCH=NO
3030
-project ${CMAKE_SOURCE_DIR}/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj
31-
-target FileProviderExt -configuration ${XCODE_TARGET_CONFIGURATION} "SYMROOT=${CMAKE_CURRENT_BINARY_DIR}"
31+
-target FileProviderExt
32+
-configuration ${XCODE_TARGET_CONFIGURATION}
33+
"SYMROOT=${CMAKE_CURRENT_BINARY_DIR}"
3234
"OC_APPLICATION_EXECUTABLE_NAME=${APPLICATION_EXECUTABLE}"
3335
"OC_APPLICATION_VENDOR=${APPLICATION_VENDOR}"
3436
"OC_APPLICATION_NAME=${APPLICATION_NAME}"

shell_integration/MacOSX/NextcloudIntegration/NextcloudDev/Build.xcconfig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@ OC_APPLICATION_NAME=$(OC_APPLICATION_NAME:default=Nextcloud)
2222
// Reverse base identifier.
2323
//
2424
OC_APPLICATION_REV_DOMAIN=$(OC_APPLICATION_REV_DOMAIN:default=com.nextcloud.desktopclient)
25+
26+
//
27+
// Hardened runtime as required for notarization.
28+
//
29+
ENABLE_HARDENED_RUNTIME=YES

0 commit comments

Comments
 (0)