Sparkle Appcast Automation in Xcode
Developer — 22 Sep 2008 16:17 — 535 days ago

Recent versions of the Sparkle auto-update framework used by many Mac OS X applications require the publisher to protect their users from malicious attacks by either delivering application updates over SSL or digitally signing them, which is a good thing.

Signing involves a few steps with OpenSSL that need to be executed again for every release build. To make this more reliable and convenient, I automated the process with a custom script phase in my “distribution” Xcode target.

The first step is to generate a DSA key pair as documented on the Sparkle website. This gives you the private key in dsa_priv.pem and the public key in dsa_pub.pem.

The private key is sensitive and I did not want it lying around on the file system, so I added it to my keychain as a Secure Note in the Keychain Access utility:

Keychain Access New Secure Note menu item

Keychain Access DSA Key secure note content

For completeness, I also added the public key even though it is not sensitive, resulting in these two secure note items:

Secure note list items

I deleted the private key from the file system, moved the public key to the resources of my Xcode project and included its name in the Info.plist file under the SUPublicDSAKeyFile key, again as documented on the Sparkle Website.

In Xcode, I added a “Distribution” shell script target:

Xcode shell script target

This is the shell script code:

set -o errexit

[ $BUILD_STYLE = Release ] || { echo Distribution target requires "'Release'" build style; false; }

VERSION=$(defaults read "$BUILT_PRODUCTS_DIR/$PROJECT_NAME.app/Contents/Info" CFBundleVersion)
DOWNLOAD_BASE_URL="http://www.example.com/download/"
RELEASENOTES_URL="http://www.example.com/software/my-cool-app/release-notes.html#version-$VERSION"

ARCHIVE_FILENAME="$PROJECT_NAME $VERSION.zip"
DOWNLOAD_URL="$DOWNLOAD_BASE_URL/$ARCHIVE_FILENAME"
KEYCHAIN_PRIVKEY_NAME="Sparkle Private Key 1"

WD=$PWD
cd "$BUILT_PRODUCTS_DIR"
rm -f "$PROJECT_NAME"*.zip
ditto -ck --keepParent "$PROJECT_NAME.app" "$ARCHIVE_FILENAME"

SIZE=$(stat -f %z "$ARCHIVE_FILENAME")
PUBDATE=$(date +"%a, %d %b %G %T %z")
SIGNATURE=$(
	openssl dgst -sha1 -binary < "$ARCHIVE_FILENAME" \
	| openssl dgst -dss1 -sign <(security find-generic-password -g -s "$KEYCHAIN_PRIVKEY_NAME" 2>&1 1>/dev/null | perl -pe '($_) = /"(.+)"/; s/\\012/\n/g') \
	| openssl enc -base64
)

[ $SIGNATURE ] || { echo Unable to load signing private key with name "'$KEYCHAIN_PRIVKEY_NAME'" from keychain; false; }

cat <<EOF
		<item>
			<title>Version $VERSION</title>
			<sparkle:releaseNotesLink>$RELEASENOTES_URL</sparkle:releaseNotesLink>
			<pubDate>$PUBDATE</pubDate>
			<enclosure
				url="$DOWNLOAD_URL"
				sparkle:version="$VERSION"
				type="application/octet-stream"
				length="$SIZE"
				sparkle:dsaSignature="$SIGNATURE"
			/>
		</item>
EOF

echo scp "'$HOME/svn/my-cool-app/build/Release/$ARCHIVE_FILENAME'" www.example.com:download/
echo scp "'$WD/appcast.xml'" www.example.com:web/software/my-cool-app/appcast.xml

You have to set the shell to /bin/bash because this code uses features that are not available otherwise.

This script:

  • enforces a Release build
  • creates a .zip file of the application. You could also package up a .dmg with additional material.
  • fetches the signing private key from the keychain. This step might pop up a dialog: keychain access dialog
  • calculates the SHA1 checksum of the distribution file
  • signs the checksum with the private key
  • converts the signature to Base64
  • emits an item block with all the information (date, size, version) about the update. You add this block to your appcast XML file.

I also let it print out the scp commands required to publish the update.

Update: Alan Craig is working on an extended Ruby version of the bash script, featuring a few more options.

Snow Leopard Update: It seems that on Snow Leopard, the security tool’s output changed. You have to replace the multiline variable assignment to SIGNATURE above with this:

SIGNATURE=$(
	openssl dgst -sha1 -binary < "$ARCHIVE_FILENAME" \
	| openssl dgst -dss1 -sign <(security find-generic-password -g -s "$KEYCHAIN_PRIVKEY_NAME" 2>&1 1>/dev/null | perl -pe '($_) = /"(.+)"/; s/\\012/\n/g' | perl -MXML::LibXML -e 'print XML::LibXML->new()->parse_file("-")->findvalue(q(//string[preceding-sibling::key[1] = "NOTE"]))') \
	| openssl enc -base64
)

This is because the output is now an XML property list with multiple items.

Update: An earlier version of this article used this command to produce the distribution ZIP file:

zip -qr "$ARCHIVE_FILENAME" "$PROJECT_NAME.app"

It turns out that this compresses inefficiently. Thanks to Fabian Jäger for the ditto line used above, which compresses more efficiently and produces smaller files.

Comments
Posted by Marmotte on 6 Oct 2008 22:18

Errr... I hope this is not your real private key in the screenshots ?

Posted by Marc on 7 Oct 2008 16:29

Hehe... I *knew* somebody was going to mention that :-) No it's a private key I generated specifically for the screenshot and then threw away.

Posted by yllan on 29 Oct 2008 21:18

Uh, I was stuck at the step using security-tool to read the private key. It says, "security: SecKeychainFindGenericPassword: The specified item could not be found in the keychain."

I add only private key(no pub key) as a secure note to keychain, and I check several times to ensure that I provided the correct KEYCHAIN_PRIVKEY_NAME.

Could you figure out what I've done wrong?

Posted by Marc on 30 Oct 2008 00:08

Hmm I have no idea... I would suggest to start over with a very simple name such as “test” and see if you can get it to work that way...

Posted by Yvan BARTHELEMY on 30 Oct 2008 11:48

Maybe you had the key renamed, then try the original name you set.

Posted by Marc on 30 Oct 2008 17:03

Yvan’s remark just reminded me that I had the same problem. When you rename a keychain item, it only seems to change the user-visible part, but there seems to be a different internal name that you can never rename.

The solution is to use the original name, as Yvan suggests, or create a fresh keychain item with a new name.

Posted by Toby on 26 Nov 2008 16:56

Hi, When you say you deleted your private key from the filesystem, I hope you used 'rm -P' - this is a good habit for removing sensitive data.

Posted by Steve on 6 Dec 2008 04:47

Thanks for this helpful post!

I think I have everything setup correctly but I guess I don't really understand the 'Distribution Script. I have created the new target and embedded your text above and customized it for my context.

After successfully building a 'Release' version of my app, I open the context menu and 'Build and Start' the target. I never see the values echoed at the bottom of the script. Instead my app just launches... If I do this with a 'Debug' build then I do see the error text at the top of the script reminding me that this is for 'Release'...

What am I goofing up?
Thanks!!
Steve

Posted by Steve on 6 Dec 2008 06:00

OK I have another question...

At the end of the script you assemble what seems to be a new item entry for the appcast and it looks like it is inside a CAT command...
So it seems like you are attempting to add a new entry to an existing appcast.xml file.. But where is the reference to that file so that the contents of your CAT get appended...

Later in the script you seem assume that an appcast file exist in the $BUILT_PRODUCTS_DIR, is the file your intend to append to?

Am I misreading this?
I don't see anything get added to an appcast file if I do place it in the $BUILD_PRODUCTS_DIR.
Is there something missing from the script?

Thanks!
Steve

Posted by Marc on 8 Dec 2008 22:07

Steve,

1.) It sounds like you added that shell script phase to your existing application target but that’s not where it needs to go.

In my projects, I usually create a second target called “Distribution” that only concerns itself with creating the distribution files and appcast stuff.

That second target has a dependency on the main build target. By separating the two, I move all the rarely-used distribution activities out of the way for my frequent builds of the main application.

To create this second target, use the Add -> New Target... command and pick a “Shell Script Target”. Then add the shell script there.

Now when you are done developing and testing your app using the main app target, you switch to the Distribution target and the Release build style.

You only ever “build” this target, you never “run” it because it does not produce an executable.

2.) Regarding your second question: The script is actually more low-tech than what you seem to assume. It only spits out an item entry for the appcast feed for the current version, but I have to copy/paste this manually from the build log output in Xcode into the appcast.xml file. I actually prefer it this way to make sure I never accidentally trigger an application update.

Posted by Steve on 8 Dec 2008 22:51

Marc;

Thanks for the response!
I had built a second target just as you describe above.
When I build this target all I ever see in the 'Build' segment is "Build Succeeded' ...
(I cntl-Clcik on the 'BigRedTarget' and select 'Build')
(This target has one 'RunScript' step - your script)
There is nothing in the Console.app 'Console Messages' either.
I'm using Leopard/XC3 in the 'All-In-One' layout mode.

I appreciate the concern over inadvertently tripping an app update.

So if I could just figure out where the output is being routed so I could examine it, I think I might be good to go!
Any thoughts on what's wrong?

Posted by Marc on 8 Dec 2008 23:01

Right, I forgot to mention how to show the console output in Xcode.

In the build view there is a tiny icon below the part that tells you the status of the build. It looks like a few rows of text. You can see it here:

http://skitch.com/liyanage/77up/xcode-console-output

If you click that icon, an additional pane with all the console text opens up. Note that Xcode sometimes scales this out of reach if you resize the window. If that happens you need to fiddle around with the divider line of the pane above it.

Posted by Fabian Jäger on 23 Jun 2009 22:52

Hey guys,
is anybody of you using Snow Leopard? I encountered some problems with the script when it comes to reading the secure note from the Keychain application. It seems that the returned string is not formatted the way the script expects it to be. Therefore it cannot extract the key from the note. Has anyone worked around this issue already? Would be great!

Fabian

Powered By blojsom