nw.js icon indicating copy to clipboard operation
nw.js copied to clipboard

Notarize OSX / Hardened Runtime

Open marcgfx opened this issue 6 years ago • 42 comments

I'm trying to understand what needs to be done, so that I can notarize my NWjs application. I am using 33.3 as I have prebuilt binaries for that version and it seems to be nice and stable.

I can't seem to find anything related to notarization and NWjs and I'm struggling to follow any steps due to terrible information from the notarization tool. I tried to notarize via command line:

xcrun altool --notarize-app --primary-bundle-id "space.devader.demo" --file "../DevaderSrc/nwjs-v0.33.3-osx-x64/devader.app" --output-format xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>os-version</key>
	<string>10.14.5</string>
	<key>product-errors</key>
	<array>
		<dict>
			<key>code</key>
			<integer>-1</integer>
			<key>message</key>
			<string>The operation couldn’t be completed. ( error -1.)</string>
		</dict>
	</array>
	<key>tool-path</key>
	<string>/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework</string>
	<key>tool-version</key>
	<string>1.1.1138</string>
</dict>
</plist>

marcgfx avatar Jul 17 '19 15:07 marcgfx

+1 On this. Electron apparently has the 'electron-notarize' project, maybe some hints there at how to do the same for NWJS?

thedracle avatar Jul 22 '19 03:07 thedracle

This was being discussed on the mailing list. It was waiting on a chromium bug to be resolved, but I'm not sure on the current status. The mailing list thread could be found in the link below:

https://groups.google.com/forum/#!topic/nwjs-general/PjoEAf0D72A

bluthen avatar Jul 22 '19 18:07 bluthen

I was able to fully notarize our nwjs app and get it approved by Apple! Thanks to the mailing list topic above.

I do had to sign all of the included nwjs frameworks and dylibs of the different libraries, separately! Which was a huge list! So for the convenience I made a bash script that finds all the modules and sign them.

Script is called codesign.sh that does all that automatically:

dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
app="$dir/your_app.app"
identity="**Your Apple registered Identity (XXXX)**"

echo "### signing app"
ITEMS=""

FRAMEWORKS_DIR="$app/Contents"
if [ -d "$FRAMEWORKS_DIR" ] ; then
    FRAMEWORKS=$(find "${FRAMEWORKS_DIR}" -depth -type d -name "*.framework" -or -type d -name "*.app" -or -type d -name "*.xpc" -or -name "*.dylib" -or -name "*.bundle" -or -path "*/Helpers/*" | sed -e "s/\(.*\/\(.*\)\.framework\)$/\1\/Versions\/A\/\2/")
    #  
    RESULT=$?
    if [[ $RESULT != 0 ]] ; then
        exit 1
    fi

    ITEMS="${FRAMEWORKS}"
fi

echo "Found:"
echo "${ITEMS}"

# Change the Internal Field Separator (IFS) so that spaces in paths will not cause problems below.
SAVED_IFS=$IFS
IFS=$(echo -en "\n\b")

# Loop through all items.
for ITEM in $ITEMS;
do
    echo "Signing '${ITEM}'"
    codesign --verbose --force --deep --strict --options runtime --timestamp --sign "$identity" --entitlements neededToRun.entitlements "${ITEM}"
    RESULT=$?
    if [[ $RESULT != 0 ]] ; then
        echo "Failed to sign '${ITEM}'."
        IFS=$SAVED_IFS
        exit 1
    fi
done

# Restore $IFS.
IFS=$SAVED_IFS

codesign --verbose --force --deep --strict --options runtime --timestamp --sign "$identity" --entitlements neededToRun.entitlements "$app"

echo "### verifying signature"
codesign -vvv -d "$app"

also as neededToRun.entitlements file I have:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.automation.apple-events</key>
    <true/>
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-executable-page-protection</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
  </dict>
</plist>

for the further upload and notarization by apple I used the commands described in: https://successfulsoftware.net/2018/11/16/how-to-notarize-your-software-on-macos/

and it all worked great.

Maybe this should be included in the docs @rogerwang ? As from the next MacOS release 10.15 now in September, notarization will be required and nwjs apps won't be able to run if not notarized by Apple!

Important

Beginning in macOS 10.14.5, all new or updated kernel extensions and all software from developers new to distributing with Developer ID must be notarized in order to run. Beginning in macOS 10.15, notarization is required by default for all software.

https://developer.apple.com/documentation/security/notarizing_your_app_before_distribution

gpetrov avatar Aug 12 '19 14:08 gpetrov

When I use the script write by @gpetrov I get a error message below:

### signing app
Found:
/Users/macmini/Desktop/NW.js/OXWU/dist/abc.app/Contents/Versions/69.0.3497.100/nwjs Helper.app
/Users/macmini/Desktop/NW.js/OXWU/dist/abc.app/Contents/Versions/69.0.3497.100/nwjs Framework.framework/Versions/A/Resources/app_mode_loader.app
/Users/macmini/Desktop/NW.js/OXWU/dist/abc.app/Contents/Versions/69.0.3497.100/nwjs Framework.framework/Versions/A/Libraries/libEGL.dylib
/Users/macmini/Desktop/NW.js/OXWU/dist/abc.app/Contents/Versions/69.0.3497.100/nwjs Framework.framework/Versions/A/Libraries/libswiftshader_libEGL.dylib
/Users/macmini/Desktop/NW.js/OXWU/dist/abc.app/Contents/Versions/69.0.3497.100/nwjs Framework.framework/Versions/A/Libraries/libGLESv2.dylib
/Users/macmini/Desktop/NW.js/OXWU/dist/abc.app/Contents/Versions/69.0.3497.100/nwjs Framework.framework/Versions/A/Libraries/libswiftshader_libGLESv2.dylib
/Users/macmini/Desktop/NW.js/OXWU/dist/abc.app/Contents/Versions/69.0.3497.100/nwjs Framework.framework/Versions/A/XPCServices/AlertNotificationService.xpc
/Users/macmini/Desktop/NW.js/OXWU/dist/abc.app/Contents/Versions/69.0.3497.100/nwjs Framework.framework/Versions/A/Helpers/crashpad_handler
/Users/macmini/Desktop/NW.js/OXWU/dist/abc.app/Contents/Versions/69.0.3497.100/nwjs Framework.framework/libnode.dylib
/Users/macmini/Desktop/NW.js/OXWU/dist/abc.app/Contents/Versions/69.0.3497.100/nwjs Framework.framework/Versions/A/nwjs Framework
/Users/macmini/Desktop/NW.js/OXWU/dist/abc.app/Contents/Versions/69.0.3497.100/libffmpeg.dylib
Signing '/Us'
/Us: No such file or directory
Failed to sign '/Us'.

Did I miss something? Thank you.

weiluenju avatar Jan 11 '20 17:01 weiluenju

SUCCESS! After two days of struggle.

After a bunch of struggling with @gpetrov's script (it kept on matching things it shouldn't sign), I rewrote it in Node and hardcoded more paths.

@weiluenju, you might want this, too.

sign-mac-app or whatever else you want to call the shell script; remember to chmod 755.

#!/usr/bin/env node

const APP = "path/to/your/app.app";
const IDENTITY = "identity of Developer ID cert";

/****************************************************************************/

console.log("### finding things to sign");

const fs = require('fs');
const child_process = require('child_process');

const items = [];

const frameworksDir = `${APP}/Contents/Frameworks/nwjs Framework.framework`;

let currentVersionDir;
for (const dir of fs.readdirSync(`${frameworksDir}/Versions`)) {
    if (fs.statSync(`${frameworksDir}/Versions/${dir}`).isDirectory) {
        currentVersionDir = `${frameworksDir}/Versions/${dir}`;
        break;
    }
}
if (!currentVersionDir) {
    console.error(`couldn't find "${frameworksDir}/Versions/[version]"`);
    process.exit(1);
}
for (const file of fs.readdirSync(`${currentVersionDir}`)) {
    if (file.endsWith('.dylib')) {
        items.push(`${currentVersionDir}/${file}`);
    }
}
for (const file of fs.readdirSync(`${currentVersionDir}/Helpers`)) {
    if (/^[a-z0-9_]*$/.test(file) || file.endsWith('.app')) {
        items.push(`${currentVersionDir}/Helpers/${file}`);
    }
}
for (const file of fs.readdirSync(`${currentVersionDir}/Libraries`)) {
    if (file.endsWith('.dylib')) {
        items.push(`${currentVersionDir}/Libraries/${file}`);
    }
}
for (const file of fs.readdirSync(`${currentVersionDir}/XPCServices`)) {
    if (file.endsWith('.xpc')) {
        items.push(`${currentVersionDir}/XPCServices/${file}`);
    }
}
items.push(frameworksDir);

/****************************************************************************/

console.log("");
console.log("### signing");

function exec(cmd) {
    console.log(cmd);
    const result = child_process.spawnSync(cmd, {shell: true, stdio: 'inherit'});
    if (result.status !== 0) {
        console.log(`Command failed with status ${result.status}`);
        if (result.error) console.log(result.error);
        process.exit(1);
    }
}

for (const item of items) {
    exec(`codesign --verbose --force --deep --strict --options runtime --timestamp --sign "${IDENTITY}" --entitlements neededToRun.entitlements "${item}"`);
}

exec(`codesign --verbose --force --deep --strict --options runtime --timestamp --sign "${IDENTITY}" --entitlements neededToRun.entitlements "${APP}"`);

/****************************************************************************/

console.log("");
console.log("### verifying signature");

exec(`codesign --verify -vvvv "${APP}"`);

neededToRun.entitlements is still needed.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.automation.apple-events</key>
    <true/>
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-executable-page-protection</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
  </dict>
</plist>

Zarel avatar Feb 04 '20 04:02 Zarel

Thnaks @Zarel But I still use the previous(0.33.4). The folder structure seems different from your version cause an error in codesign. For example, the "nwjs Framework.framework" full path is "xxx.app/Contents/Versions/69.0.3497.100/nwjs Framework.framework" in 0.33.4.

weiluenju avatar Feb 23 '20 13:02 weiluenju

@weiluenju My code works for the version of NW.js I used it on. It should be pretty readable (for someone using NW.js and so familiar with JavaScript) and easy to modify if your version has the files in different locations.

items is an array of paths to things to sign, so just add and remove entries in it until it works for you.

Zarel avatar Feb 24 '20 00:02 Zarel

Thanks @Zarel I successfully codesign my program. But when I execute my program after codesign, it crash. Unless I remove the "--options runtime" parameter. But without the parameter, notarization will failed(The executable does not have the hardened runtime enabled.) How should I do?

weiluenju avatar Mar 02 '20 08:03 weiluenju

Sorry, I'm no expert. All I can say is that it worked for me when I used it.

Are you using the same neededToRun.entitlements? That's the only thing I can think of.

Zarel avatar Mar 02 '20 08:03 Zarel

I had a similar issue. Two things that helped me to find a working solution:

  • Don't set com.apple.security.app-sandbox entitlement to true. You need sandboxing for Mac App Store and hardened runtime for notarization. Having both is not required (yet). Hardened runtime and sandboxing are two different things, read more about this https://lapcatsoftware.com/articles/hardened-runtime-sandboxing.html
  • Use the same entitlements for both the main app and nested binaries (like is suggested above by @gpetrov and @Zarel ). Don't use com.apple.security.inherit entitlement for nested code like suggested in MAS instructions, at least it didn't work for me.

Here is my entitlements file, yours might need to be different:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<false/>
	<key>com.apple.security.network.client</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
	<key>com.apple.security.files.user-selected.read-write</key>
	<true/>
	<key>com.apple.security.device.camera</key>
	<true/>
	<key>com.apple.security.files.downloads.read-write</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
	<true/>
</dict>
</plist>

tonyofbyteball avatar Mar 03 '20 11:03 tonyofbyteball

Unfortunately I still failed. Even I remove all the nodejs part, just keep one static html file here.

Can @Zarel and @tonyofbyteball tell me Which nwjs version do you use?

And what the nwjs builder do you use?

I want to build a same environment to test.

Thank you.

weiluenju avatar Mar 09 '20 06:03 weiluenju

Looks like I'm using 0.43.6, SDK flavor.

Zarel avatar Mar 09 '20 17:03 Zarel

Thanks @Zarel , and which builder did you use? nw-builder or nwjs-builder-phoenix?

Looks like I'm using 0.43.6, SDK flavor.

weiluenju avatar Mar 10 '20 02:03 weiluenju

What's a builder? I just used the dist on the website: https://dl.nwjs.io/v0.43.6/nwjs-sdk-v0.43.6-osx-x64.zip

Zarel avatar Mar 10 '20 04:03 Zarel

A builder can help you package you nwjs program into a single xxx.app file automatically.

http://docs.nwjs.io/en/latest/For%20Users/Package%20and%20Distribute/

What's a builder? I just used the dist on the website: https://dl.nwjs.io/v0.43.6/nwjs-sdk-v0.43.6-osx-x64.zip

weiluenju avatar Mar 10 '20 05:03 weiluenju

Oh, I just dragged files into the bundle and changed plist files manually. It looks like nw-builder is super outdated and nwjs-builder-phoenix doesn't support Mac?

Zarel avatar Mar 10 '20 16:03 Zarel

@Zarel Hi Zarel, could you please share your scripts and all entitlements please? I tried the scripts above it's still have the problem.

I tried to codesign the original nwjs (nwjs-sdk-v0.44.5-osx-x64) as well. codesign verify all passed, but when I run the signed app, it gives me CODESIGNING CODE 0x1

Crashed Thread:        0

Exception Type:        EXC_CRASH (Code Signature Invalid)
Exception Codes:       0x0000000000000000, 0x0000000000000000
Exception Note:        EXC_CORPSE_NOTIFY

Termination Reason:    Namespace CODESIGNING, Code 0x1

thehovercam avatar Mar 31 '20 23:03 thehovercam

@thehovercam I already did. They're in this comment: https://github.com/nwjs/nw.js/issues/7117#issuecomment-581738908

Zarel avatar Apr 01 '20 07:04 Zarel

@Zarel Thank you for your reply. I tried that on nwjs.app as well, after sign, nwjs.app got the CODESIGNING ERROR. I'm not sure which part I was missing. Could you please do a quick test on nwjs.app, let me know what's the files you changed in content of nwjs.app as well? Maybe I missed some plist files, Thanks.

@thehovercam I already did. They're in this comment: #7117 (comment)

thehovercam avatar Apr 01 '20 07:04 thehovercam

@Zarel You're the best ! Simple and efficient signing script, perfectly works with apple new notarize standards. Great. Do you think it could add an icon to the signed file too ?

ACC-Math avatar May 26 '20 17:05 ACC-Math

@ACC-Math Adding icons isn't part of the notarization process; you add them before signing.

Adding icons is documented here:

https://github.com/nwjs/nw.js/wiki/How-to-package-and-distribute-your-apps#mac-os-x

Zarel avatar May 30 '20 04:05 Zarel

After successful notarizing with the script above OS X still shows "Application can't be opened because Apple cannot check it for malicious software"

Any ideas on why it happens and how to fix this?

Edit – For those who might have the same issue I was able to notarize the app using https://github.com/mitchellh/gon after application was signed using the scripts above. For our case entitlements file wasn't needed at all for code signing, but additionally we needed to sign additional binaries used in app (in our case inside app.nw/bin) and few node_modules binaries need to be signed. You'll see binaries needed to be signed in output of gon command

IharKrasnik avatar Aug 19 '20 10:08 IharKrasnik

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Jan 09 '22 01:01 stale[bot]

I think it should be kept open. NW.js still doesn't have official support for notarization, as far as I'm aware.

Zarel avatar Jan 09 '22 02:01 Zarel

I updated @Zarel 's script above from 2020 to work for NWJS v0.73.0.

Here's the patch file:

--- orig.js	2023-02-27 22:14:24.000000000 +0100
+++ new.js	2023-02-27 22:17:42.000000000 +0100
@@ -30,11 +30,18 @@
         items.push(`${currentVersionDir}/${file}`);
     }
 }
+let chromiumUpdaterApp;
 for (const file of fs.readdirSync(`${currentVersionDir}/Helpers`)) {
     if (/^[a-z0-9_]*$/.test(file) || file.endsWith('.app')) {
         items.push(`${currentVersionDir}/Helpers/${file}`);
+        if (file === "ChromiumUpdater.app") {
+            chromiumUpdaterApp = `${currentVersionDir}/Helpers/${file}`;
+        }
     }
 }
+if (chromiumUpdaterApp) {
+    items.push(`${chromiumUpdaterApp}/Contents/Helpers/ChromiumSoftwareUpdate.bundle/Contents/Resources/ChromiumSoftwareUpdateAgent.app`);
+}
 for (const file of fs.readdirSync(`${currentVersionDir}/Libraries`)) {
     if (file.endsWith('.dylib')) {
         items.push(`${currentVersionDir}/Libraries/${file}`);
@@ -45,6 +52,11 @@
         items.push(`${currentVersionDir}/XPCServices/${file}`);
     }
 }
+const
+   updateHelperPath = `${APP}/Contents/Library/LaunchServices/org.chromium.Chromium.UpdaterPrivilegedHelper`;
+if (fs.existsSync(updateHelperPath)) {
+    items.push(updateHelperPath);
+}
 items.push(frameworksDir);
 
 /****************************************************************************/

semmel avatar Feb 27 '23 21:02 semmel

@semmel was this tested? I'm not finding Contents/Frameworks/nwjs Framework.framework/Versions/110.0.5481.97/XPCServices folder so the script fails

zkrige avatar Mar 01 '23 13:03 zkrige

@zkrige tested with nwjs-sdk, will look to make it more general and will report if it works with regular nwjs…

Where did you encounter XPCServices? ~~It is not referenced in the script, or is it?~~ Found it: It's from the original script.

semmel avatar Mar 01 '23 14:03 semmel

@semmel ta - I just commented out the XPCServices block and it seems to work fine ¯_(ツ)_/¯

zkrige avatar Mar 01 '23 14:03 zkrige

@zkrige Yes you are right. This is the updated patchfile:

--- orig.js	2023-02-27 22:14:24.000000000 +0100
+++ new.js	2023-03-01 16:27:30.000000000 +0100
@@ -30,20 +30,27 @@
         items.push(`${currentVersionDir}/${file}`);
     }
 }
+let chromiumUpdaterApp;
 for (const file of fs.readdirSync(`${currentVersionDir}/Helpers`)) {
     if (/^[a-z0-9_]*$/.test(file) || file.endsWith('.app')) {
         items.push(`${currentVersionDir}/Helpers/${file}`);
+        if (file === "ChromiumUpdater.app") {
+            chromiumUpdaterApp = `${currentVersionDir}/Helpers/${file}`;
+        }
     }
 }
+if (chromiumUpdaterApp) {
+    items.push(`${chromiumUpdaterApp}/Contents/Helpers/ChromiumSoftwareUpdate.bundle/Contents/Resources/ChromiumSoftwareUpdateAgent.app`);
+}
 for (const file of fs.readdirSync(`${currentVersionDir}/Libraries`)) {
     if (file.endsWith('.dylib')) {
         items.push(`${currentVersionDir}/Libraries/${file}`);
     }
 }
-for (const file of fs.readdirSync(`${currentVersionDir}/XPCServices`)) {
-    if (file.endsWith('.xpc')) {
-        items.push(`${currentVersionDir}/XPCServices/${file}`);
-    }
+const
+   updateHelperPath = `${APP}/Contents/Library/LaunchServices/org.chromium.Chromium.UpdaterPrivilegedHelper`;
+if (fs.existsSync(updateHelperPath)) {
+    items.push(updateHelperPath);
 }
 items.push(frameworksDir);

semmel avatar Mar 01 '23 15:03 semmel

@semmel perfect - thank you!

zkrige avatar Mar 02 '23 05:03 zkrige