Search code examples
macoscode-signingpackagingwine

MacOS: Why does 'spctl --assess' return 'no usable signature' on a PortJump package, after signing and notarizing have seemingly succeeded?


[Note: Skip to Update below if the background info is of no concern to you]

I have an open source accounting software for Windows and had a Mac port built by Codeweavers (Wine and Crossover working under the hood). They are smart, nice, and helpful people but have zero documentation for their customers on their PortJump product (and there is nothing to find elsewhere on the internet, although Codeweavers claim to have made ports a thousand times). Maybe my question is too noobish for them to even grasp my situation. So I tried for myself for months now and desperation leads to longer and longer gaps between my feeble attempts.

I have an .app package in a .zip file you can find here if you want:

https://www.codeweavers.com/xfer/oems/EasyCashTax/easyct-2.38.3-unsigned.zip

Access Code: MUTmlUVm

On the Apple developer portal, I created a distribution identity with the Certificate Name "Thomas Mielke" and a provisioning profile for app id "de.easyct.easyct". (I also have a X.509 software code signing certificate from an official CA, if needed.)

Maybe the first thing I should do is to sign the code, like in this question: codesign --deep on mavericks xcode 5.0 (5A1412)

Or maybe this shouldn't be the first step at all... I am completely alienated by the whole Mac environment and always feel like there are too many open questions at once to just start hacking (why is this a zip and no dmg? how deep do I have to sign and with what options? why can't this be a project I can simply open in Xcode and sign using the Organizer?).

Maybe someone could guide me to a safe place for me where I can start to feel comforable and enter into happy try&error loops... Or, in other words: If you would have to maintain a PortJump package, what would be your approach: git repo, homebrew something, Xcode something, shell scripting or use other software?

Is there a Mac developer out here who can show me the beauty and power of developing on MacOS?

Update:

I now got this script to sign my package:

#!/bin/bash

MAC_SIGNING_IDENTITY="Developer ID Application:"
entitlements="wine32on64.entitlements"
app="$1"
product_id=
bundle_id=
SRCROOT=.

if [ ! -f $entitlements ]
then
    echo "$entitlements not found. Make sure it's in your working directory."
        exit 1
fi

if [ -z "$app" ]
then
    echo "You must specify the absolute path to the .app"
        exit 1
fi

if [ ! -d "$app" ]
then
    echo "The path You specify is invalid. Please provide the absolute path to the .app"
        exit 1
fi

if [[ ! "$app" = /* ]]
then
    echo "The path you specified is not an absolute path. Please provide the absolue path to the .app"
        exit 1
fi

if [ -z "$bundle_id" ]
then
    bundle_id=`defaults read "$app/Contents/Info.plist" CFBundleIdentifier`
    if [ -z "$bundle_id" ]
    then
        echo "Could not determine the product name from '$app'. Did you provide the absolute path to the .app?"
        exit 1
    fi
fi
echo "Bundle ID = \"$bundle_id\""

if [ -z "$product_id" ]
then
    product_id=`ls -d "$app/Contents/SharedSupport"/* | grep -v '/X11'`
    if [ ! -d "$product_id" ]
    then
        echo "could not determine the product id from '$app'"
        exit 1
    fi
    product_id=`basename "$product_id"`
    echo "$product_id" | LC_ALL=C egrep '^[a-zA-Z0-9_][a-zA-Z0-9_][a-zA-Z0-9_][a-zA-Z0-9_][a-zA-Z0-9_]*$' >/dev/null
    if [ $? -ne 0 ]
    then
        echo "the product id '$product_id' is not valid"
        exit 1
    fi
fi
echo "Product ID   = \"$product_id\""

keychain=$(security find-certificate -c "$MAC_SIGNING_IDENTITY" | grep keychain | awk 'gsub(/"/, "", $2) {print $2}')
locked=$(security show-keychain-info "$keychain" 2>&1 | grep "timeout")
if [ -z "$locked" ]
then
        echo "Failed to find unlocked keychain with required certificate. Is your certificate in an unlocked keychain in your keychain search path?"
        echo "Your keychain search path is:"
    security list-keychain
        exit 1
fi

if [ "$MAC_SIGNING_IDENTITY" != "-" ] ; then
    # Figure out the Organizational Unit (OU) from the signing identity
    ou=$(
        set -x
        security find-certificate -p -c "$MAC_SIGNING_IDENTITY" | \
            openssl x509 -inform PEM -subject -noout -nameopt sname,sep_multiline,space_eq | \
            awk '/ OU = / {print $3}'
    )

    if [ -z "$ou" ]; then
        echo "error: Could not determine OU from signing identity '$MAC_SIGNING_IDENTITY'"
        exit 1
    fi
fi

set -e

# Sign the app.  The designated requirements were obtained by watching what Xcode 4.3
# does when it signs for Developer ID.
function sign_one()
{
    file="$1"; shift
    identifier="$1"; shift
    if [ "$MAC_SIGNING_IDENTITY" = "-" ] ; then
        codesign --sign "$MAC_SIGNING_IDENTITY" \
            --force \
            "$file" "$@"
    else
        codesign --sign "$MAC_SIGNING_IDENTITY" \
            --force \
            --requirements "=designated => anchor apple generic and identifier \"$identifier\" \
               and ((cert leaf[field.1.2.840.113635.100.6.1.9] exists) or \
                    (certificate 1[field.1.2.840.113635.100.6.2.6] exists and \
                      certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = \"$ou\" \
                    ))" \
            "$file" "$@"
    fi
}

function sign_subdir()
{
  subdir="$1" ; shift
  id_component="$1" ; shift
    
  find "$subdir/" -type f \( -name "*.so" -o -name "*dylib" -o -exec sh -c 'file "$0" | fgrep -qsw Mach-O' {} \; \) -print0 |
    while IFS= read -r -d '' file ; do
      name=$(basename "$file")
      name="${name//[^-a-zA-Z0-9]/-}"
      if [ -z "${name/#[^a-zA-Z]*}" ] ; then
        name="a-$name"
      fi
      if [ -z "${name/%*[^a-zA-Z0-9]}" ] ; then
        name="$name-0"
      fi
      identifier="$bundle_id.$id_component.$name"
      sign_one "$file" "$identifier" --identifier "$identifier" "$@"
    done
}

set -x

# Sign Sparkle framework and pyobjc bundle separately from the app bundle
if [ -d "$app/Contents/Frameworks/Sparkle.framework" ]; then
  sign_one "$app/Contents/Frameworks/Sparkle.framework/Versions/A/Resources/finish_installation.app" "org.andymatuschak.sparkle.finish-installation" --options runtime
  sign_one "$app/Contents/Frameworks/Sparkle.framework" "org.andymatuschak.Sparkle"
fi

sign_subdir "$app/Contents/SharedSupport/$product_id/bin" "bin" --options runtime

for libdir in "$app/Contents/SharedSupport/$product_id"/lib* ; do
  sign_subdir "$libdir" "$(basename "$libdir")"
done

# The wine (pre)loaders were already signed with the bin directory, above, but
# we need to re-do it with entitlements

for i in "$app/Contents/SharedSupport/$product_id/bin"/wine*loader*; do
    sign_one "$i" "$bundle_id.wineloader" \
        --options runtime \
        --entitlements "$SRCROOT/wine32on64.entitlements"
done

sign_one "$app" "$bundle_id" --options runtime --entitlements "$SRCROOT/wine32on64.entitlements"

The entitlements file wine32on64.entitlements:

<?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-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/>
        <key>com.apple.security.device.audio-input</key>
        <true/>
        <key>com.apple.security.device.camera</key>
        <true/>
        <key>com.apple.security.ldt-in-64bit-process</key>
        <true/>
</dict>
</plist>

And this one does the notarization:

#!/bin/bash

ditto -c -k --keepParent EasyCT.app EasyCT.zip
output=$(xcrun altool --notarize-app --primary-bundle-id de.easyct.easyct --asc-provider "MYTEAMID" -u "[email protected]" -p "abcd-efgh-ijkl-mnop" --file EasyCT.zip)
ticket_id=$(echo "$output" | grep RequestUUID | awk '{print $3}')

if [ -z "$ticket_id" ]
then
    echo "Error: No ticket id was returned.\n\n$output"
        exit 1
fi

echo "Notarization ticket: $ticket_id"
xcrun altool --notarization-info "$ticket_id" -u "[email protected]" -p "abcd-efgh-ijkl-mnop"

xcrun stapler staple EasyCT.app

spctl --assess --type open --context context:primary-signature --verbose EasyCT.zip

Everything runs smooth, only the last line that checks the resulting packet using spctl --assess returns me "no usable signature". Also, after downloading the package, Gatekeeper still requires a security exception.


Solution

  • The problem turned out to be multi-factor: First, there were two symbolic links included in the package. codesign didn't bother but spctl -a didn't like it. I subsequently tested the .zip package with spctl, which didn't pass the package even after the symlinks were deleted. At some point I tried to spctl -a the uncompressed .app folder, which worked. So spctl doesn't like .zip packages, it seems.

    Here is a tidied-up version of the script I now use to sign and notarize the package:

    #!/bin/bash
    
    # read config
    echo "Reading myappleid.config"
    . ./myappleid.config
    if [ -z "$appleid" ]
    then
            echo "Error: Please add an entry 'appleid=<your_appleid_here>' to myappleid.config"
            exit 1
    fi
    if [ -z "$aspw" ]
    then
            echo "Error: Please add an entry 'aspw=<your_app_specific_password_here>' to myappleid.config"
            exit 1
    fi
    echo "Notarisation will use apple id '$appleid'"
    
    # unzip archive to EasyCash&Tax.app folder
    echo "Unzipping original package..."
    rm -rf EasyCash\&Tax.app
    unzip -q easyct-2.38.3-unsigned.zip
    
    # delete symlinks that would prevent Gatekeeper/spctl from passing otherwise
    echo "Deleting symlinks"
    rm EasyCash\&Tax.app/Contents/SharedSupport/easyct/support/easyct/drive_c/users/crossover/Downloads
    rm EasyCash\&Tax.app/Contents/SharedSupport/easyct/support/easyct/drive_c/users/crossover/Templates
    
    # sign the package using Codeweavers script
    echo "Signing package..."
    ./sign_codeV4 $(pwd)/EasyCash\&Tax.app
    
    # archive signed package to a zip for notarisation
    ditto -c -k --keepParent EasyCash\&Tax.app EasyCT.zip
    
    # prepare option if team id was set
    if [ -z "$ascprov" ]
    then
        ascprovoption=
    else
        ascprovoption="--asc-provider"
    fi
    
    output=$(xcrun altool --notarize-app --primary-bundle-id de.easyct.easyct $ascprovoption $ascprov -u "$appleid" -p "$aspw" --file EasyCT.zip)
    
    ticket_id=$(echo "$output" | grep RequestUUID | awk '{print $3}')
    
    if [ -z "$ticket_id" ]
    then
        echo "Error: No ticket id was returned.\n\n$output"
            exit 1
    fi
    
    echo "Notarization ticket: $ticket_id"
    xcrun altool --notarization-info "$ticket_id" -u "$appleid" -p "$aspw"
    
    echo "Stapeling..."
    output=$(xcrun stapler staple EasyCash\&Tax.app)
    echo $output
    stapling_worked=(echo "$output" | grep "The staple and validate action worked")
    if [ -z "$stapling_worked" ]
    then
            echo "Error: stapling didn't work, it seems. Try to run 'xcrun stapler staple EasyCash\\\&Tax.app' and zip EasyCT4Mac.zip manually."
            exit 1
    fi
    
    echo "Checking stapled EasyCash&Tax.app folder using spctl -a..."
    spctl --assess --type open --context context:primary-signature --verbose EasyCash\&Tax.app
    
    echo "Final packaging to EasyCT4Mac.zip..."
    ditto -c -k --keepParent EasyCash\&Tax.app EasyCT4Mac.zip
    
    # clean-up temporary zip, used for notarisation
    rm EasyCT.zip
    

    Hope this might help someone with similar problems.